Merge pull request #3738 from alexandrevryghem/w2p-117573_remove-observable-function-calls-from-template_contribute-8_x

[Port dspace-8_x] Removed observable function calls from template (part 1)
This commit is contained in:
Tim Donohue
2024-12-18 08:59:34 -06:00
committed by GitHub
32 changed files with 920 additions and 925 deletions

View File

@@ -61,7 +61,7 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page" <tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}"> [ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
<td>{{epersonDto.eperson.id}}</td> <td>{{epersonDto.eperson.id}}</td>
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td> <td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
<td>{{epersonDto.eperson.email}}</td> <td>{{epersonDto.eperson.email}}</td>

View File

@@ -100,6 +100,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/ */
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any); ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
activeEPerson$: Observable<EPerson>;
/** /**
* An observable for the pageInfo, needed to pass to the pagination component * An observable for the pageInfo, needed to pass to the pagination component
*/ */
@@ -165,6 +167,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
initialisePage() { initialisePage() {
this.searching$.next(true); this.searching$.next(true);
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
this.activeEPerson$ = this.epersonService.getActiveEPerson();
this.subs.push(this.ePeople$.pipe( this.subs.push(this.ePeople$.pipe(
switchMap((epeople: PaginatedList<EPerson>) => { switchMap((epeople: PaginatedList<EPerson>) => {
if (epeople.pageInfo.totalElements > 0) { if (epeople.pageInfo.totalElements > 0) {
@@ -232,23 +235,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
); );
} }
/**
* Checks whether the given EPerson is active (being edited)
* @param eperson
*/
isActive(eperson: EPerson): Observable<boolean> {
return this.getActiveEPerson().pipe(
map((activeEPerson) => eperson === activeEPerson),
);
}
/**
* Gets the active eperson (being edited)
*/
getActiveEPerson(): Observable<EPerson> {
return this.epersonService.getActiveEPerson();
}
/** /**
* Deletes EPerson, show notification on success/failure & updates EPeople list * Deletes EPerson, show notification on success/failure & updates EPeople list
*/ */

View File

@@ -2,7 +2,7 @@
<div class="group-form row"> <div class="group-form row">
<div class="col-12"> <div class="col-12">
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div> <div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div>
<ng-template #createHeader> <ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1> <h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
@@ -44,7 +44,7 @@
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading> <ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
<div *ngIf="epersonService.getActiveEPerson() | async"> <div *ngIf="activeEPerson$ | async">
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2> <h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading> <ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
@@ -75,7 +75,9 @@
{{ dsoNameService.getName(group) }} {{ dsoNameService.getName(group) }}
</a> </a>
</td> </td>
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td> <td class="align-middle">
{{ dsoNameService.getName((group.object | async)?.payload) }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@@ -19,12 +19,10 @@ import {
import { import {
ActivatedRoute, ActivatedRoute,
Router, Router,
RouterModule,
} from '@angular/router'; } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { import { TranslateModule } from '@ngx-translate/core';
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { import {
Observable, Observable,
of as observableOf, of as observableOf,
@@ -49,7 +47,6 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se
import { FormComponent } from '../../../shared/form/form.component'; import { FormComponent } from '../../../shared/form/form.component';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
@@ -92,9 +89,6 @@ describe('EPersonFormComponent', () => {
ePersonDataServiceStub = { ePersonDataServiceStub = {
activeEPerson: null, activeEPerson: null,
allEpeople: mockEPeople, allEpeople: mockEPeople,
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> { getActiveEPerson(): Observable<EPerson> {
return observableOf(this.activeEPerson); return observableOf(this.activeEPerson);
}, },
@@ -228,12 +222,8 @@ describe('EPersonFormComponent', () => {
router = new RouterStub(); router = new RouterStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({ RouterModule.forRoot([]),
loader: { TranslateModule.forRoot(),
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
EPersonFormComponent, EPersonFormComponent,
HasNoValuePipe, HasNoValuePipe,
], ],
@@ -251,7 +241,7 @@ describe('EPersonFormComponent', () => {
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
EPeopleRegistryComponent, EPeopleRegistryComponent,
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.overrideComponent(EPersonFormComponent, { .overrideComponent(EPersonFormComponent, {
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] }, remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
@@ -274,37 +264,13 @@ describe('EPersonFormComponent', () => {
}); });
describe('check form validation', () => { describe('check form validation', () => {
let firstName; let canLogIn: boolean;
let lastName; let requireCertificate: boolean;
let email;
let canLogIn;
let requireCertificate;
let expected;
beforeEach(() => { beforeEach(() => {
firstName = 'testName';
lastName = 'testLastName';
email = 'testEmail@test.com';
canLogIn = false; canLogIn = false;
requireCertificate = 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'); spyOn(component.submitForm, 'emit');
component.canLogIn.value = canLogIn; component.canLogIn.value = canLogIn;
component.requireCertificate.value = requireCertificate; component.requireCertificate.value = requireCertificate;
@@ -378,15 +344,13 @@ describe('EPersonFormComponent', () => {
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
}); });
}); });
}); });
describe('when submitting the form', () => { describe('when submitting the form', () => {
let firstName; let firstName;
let lastName; let lastName;
let email; let email;
let canLogIn; let canLogIn: boolean;
let requireCertificate; let requireCertificate;
let expected; let expected;
@@ -415,6 +379,7 @@ describe('EPersonFormComponent', () => {
requireCertificate: requireCertificate, requireCertificate: requireCertificate,
}); });
spyOn(component.submitForm, 'emit'); spyOn(component.submitForm, 'emit');
component.ngOnInit();
component.firstName.value = firstName; component.firstName.value = firstName;
component.lastName.value = lastName; component.lastName.value = lastName;
component.email.value = email; component.email.value = email;
@@ -454,9 +419,17 @@ describe('EPersonFormComponent', () => {
email: email, email: email,
canLogIn: canLogIn, canLogIn: canLogIn,
requireCertificate: requireCertificate, requireCertificate: requireCertificate,
_links: undefined, _links: {
groups: {
href: '',
},
self: {
href: '',
},
},
}); });
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId)); spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
component.ngOnInit();
component.onSubmit(); component.onSubmit();
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -504,22 +477,19 @@ describe('EPersonFormComponent', () => {
}); });
describe('delete', () => { describe('delete', () => {
let ePersonId;
let eperson: EPerson; let eperson: EPerson;
let modalService; let modalService;
beforeEach(() => { beforeEach(() => {
spyOn(authService, 'impersonate').and.callThrough(); spyOn(authService, 'impersonate').and.callThrough();
ePersonId = 'testEPersonId';
eperson = EPersonMock; eperson = EPersonMock;
component.epersonInitial = eperson; component.epersonInitial = eperson;
component.canDelete$ = observableOf(true); component.canDelete$ = observableOf(true);
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson)); spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
modalService = (component as any).modalService; modalService = (component as any).modalService;
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
component.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
}); });
it('the delete button should be visible if the ePerson can be deleted', () => { it('the delete button should be visible if the ePerson can be deleted', () => {

View File

@@ -189,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/ */
canImpersonate$: Observable<boolean>; canImpersonate$: Observable<boolean>;
/**
* The current {@link EPerson}
*/
activeEPerson$: Observable<EPerson>;
/** /**
* List of subscriptions * List of subscriptions
*/ */
@@ -254,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected router: Router, 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; this.epersonInitial = eperson;
if (hasValue(eperson)) { if (hasValue(eperson)) {
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
@@ -262,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.submitLabel = 'form.submit'; this.submitLabel = 'form.submit';
} }
})); }));
}
ngOnInit() {
this.initialisePage(); this.initialisePage();
} }
@@ -272,130 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* This method will initialise the page * This method will initialise the page
*/ */
initialisePage() { initialisePage() {
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => { if (this.route.snapshot.params.id) {
this.epersonService.editEPerson(ePersonRD.payload); this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
})); 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.firstName = new DynamicInputModel({
id: 'firstName',
this.groups$ = activeEPerson$.pipe( label: this.translateService.instant(`${this.messagePrefix}.firstName`),
switchMap((eperson) => { name: 'firstName',
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { validators: {
currentPage: 1, required: null,
elementsPerPage: this.config.pageSize, },
})]); required: true,
}),
switchMap(([eperson, findListOptions]) => {
if (eperson != null) {
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
}
return observableOf(undefined);
}),
);
this.groupsPageInfoState$ = this.groups$.pipe(
map(groupsRD => groupsRD.payload.pageInfo),
);
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.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,
}, undefined, undefined, followLink('object'));
}
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.groupsPageInfoState$ = this.groups$.pipe(
map(groupsRD => groupsRD.payload.pageInfo),
);
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);
} }
/** /**
@@ -414,7 +411,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* Emit the updated/created eperson using the EventEmitter submitForm * Emit the updated/created eperson using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( this.activeEPerson$.pipe(take(1)).subscribe(
(ePerson: EPerson) => { (ePerson: EPerson) => {
const values = { const values = {
metadata: { metadata: {
@@ -533,7 +530,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. * It'll either show a success or error message depending on whether the delete was successful or not.
*/ */
delete(): void { delete(): void {
this.epersonService.getActiveEPerson().pipe( this.activeEPerson$.pipe(
take(1), take(1),
switchMap((eperson: EPerson) => { switchMap((eperson: EPerson) => {
const modalRef = this.modalService.open(ConfirmationModalComponent); const modalRef = this.modalService.open(ConfirmationModalComponent);
@@ -637,7 +634,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* Update the list of groups by fetching it from the rest api or cache * Update the list of groups by fetching it from the rest api or cache
*/ */
private updateGroups(options) { 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); this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
})); }));
} }

View File

@@ -2,7 +2,7 @@
<div class="group-form row"> <div class="group-form row">
<div class="col-12"> <div class="col-12">
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div> <div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div>
<ng-template #createHeader> <ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1> <h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
@@ -23,11 +23,15 @@
</h1> </h1>
</ng-template> </ng-template>
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning" <ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
[content]="messagePrefix + '.alert.permanent'"></ds-alert> <ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning" [content]="messagePrefix + '.alert.permanent'"></ds-alert>
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })"> <ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO">
</ds-alert> <ds-alert *ngIf="(canEdit$ | async) !== true" [type]="AlertType.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
</ds-alert>
</ng-container>
</ng-container>
<ds-form [formId]="formId" <ds-form [formId]="formId"
[formModel]="formModel" [formModel]="formModel"
@@ -39,22 +43,21 @@
<button (click)="onCancel()" type="button" <button (click)="onCancel()" type="button"
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button> class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div> </div>
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group"> <div after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.permanent" class="btn-group">
<button (click)="delete()" class="btn btn-danger delete-button" type="button"> <button (click)="delete()" class="btn btn-danger delete-button" type="button">
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}} <i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
</button> </button>
</div> </div>
</ds-form> </ds-form>
<div class="mb-5"> <ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
<ds-members-list *ngIf="groupBeingEdited !== undefined" <div class="mb-5">
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list> <ds-members-list *ngIf="groupBeingEdited !== undefined"
</div> [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined" </div>
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list> <ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
</ng-container>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@@ -23,11 +23,7 @@ import {
} from '@angular/router'; } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { import { TranslateModule } from '@ngx-translate/core';
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { import {
Observable, Observable,
@@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component';
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { import {
GroupMock, GroupMock,
GroupMock2, GroupMock2,
} from '../../../shared/testing/group-mock'; } from '../../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
import { GroupFormComponent } from './group-form.component'; import { GroupFormComponent } from './group-form.component';
import { MembersListComponent } from './members-list/members-list.component'; import { MembersListComponent } from './members-list/members-list.component';
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component'; import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
@@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
let component: GroupFormComponent; let component: GroupFormComponent;
let fixture: ComponentFixture<GroupFormComponent>; let fixture: ComponentFixture<GroupFormComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService; let builderService: FormBuilderService;
let ePersonDataServiceStub: any; let ePersonDataServiceStub: any;
let groupsDataServiceStub: any; let groupsDataServiceStub: any;
let dsoDataServiceStub: any; let dsoDataServiceStub: any;
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let notificationService: NotificationsServiceStub; let notificationService: NotificationsServiceStub;
let router; let router: RouterMock;
let route: ActivatedRouteStub;
let groups; let groups: Group[];
let groupName; let groupName: string;
let groupDescription; let groupDescription: string;
let expected; let expected: Group;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
groups = [GroupMock, GroupMock2]; groups = [GroupMock, GroupMock2];
@@ -105,6 +100,15 @@ describe('GroupFormComponent', () => {
}, },
], ],
}, },
object: createSuccessfulRemoteDataObject$(undefined),
_links: {
self: {
href: 'group-selflink',
},
object: {
href: 'group-objectlink',
},
},
}); });
ePersonDataServiceStub = {}; ePersonDataServiceStub = {};
groupsDataServiceStub = { groupsDataServiceStub = {
@@ -141,7 +145,14 @@ describe('GroupFormComponent', () => {
create(group: Group): Observable<RemoteData<Group>> { create(group: Group): Observable<RemoteData<Group>> {
this.allGroups = [...this.allGroups, group]; this.allGroups = [...this.allGroups, group];
this.createdGroup = Object.assign({}, group, { this.createdGroup = Object.assign({}, group, {
_links: { self: { href: 'group-selflink' } }, _links: {
self: {
href: 'group-selflink',
},
object: {
href: 'group-objectlink',
},
},
}); });
return createSuccessfulRemoteDataObject$(this.createdGroup); return createSuccessfulRemoteDataObject$(this.createdGroup);
}, },
@@ -223,17 +234,15 @@ describe('GroupFormComponent', () => {
return typeof value === 'object' && value !== null; return typeof value === 'object' && value !== null;
}, },
}); });
translateService = getMockTranslateService();
router = new RouterMock(); router = new RouterMock();
route = new ActivatedRouteStub();
notificationService = new NotificationsServiceStub(); notificationService = new NotificationsServiceStub();
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({ TranslateModule.forRoot(),
loader: { GroupFormComponent,
provide: TranslateLoader, ],
useClass: TranslateLoaderMock,
},
}), GroupFormComponent],
providers: [ providers: [
{ provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
@@ -249,14 +258,11 @@ describe('GroupFormComponent', () => {
{ provide: Store, useValue: {} }, { provide: Store, useValue: {} },
{ provide: RemoteDataBuildService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} }, { provide: HALEndpointService, useValue: {} },
{ { provide: ActivatedRoute, useValue: route },
provide: ActivatedRoute,
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
},
{ provide: Router, useValue: router }, { provide: Router, useValue: router },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.overrideComponent(GroupFormComponent, { .overrideComponent(GroupFormComponent, {
remove: { imports: [ remove: { imports: [
@@ -279,8 +285,8 @@ describe('GroupFormComponent', () => {
describe('when submitting the form', () => { describe('when submitting the form', () => {
beforeEach(() => { beforeEach(() => {
spyOn(component.submitForm, 'emit'); spyOn(component.submitForm, 'emit');
component.groupName.value = groupName; component.groupName.setValue(groupName);
component.groupDescription.value = groupDescription; component.groupDescription.setValue(groupDescription);
}); });
describe('without active Group', () => { describe('without active Group', () => {
beforeEach(() => { beforeEach(() => {
@@ -288,14 +294,22 @@ describe('GroupFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should emit a new group using the correct values', (async () => { it('should emit a new group using the correct values', (() => {
await fixture.whenStable().then(() => { expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
expect(component.submitForm.emit).toHaveBeenCalledWith(expected); name: groupName,
}); metadata: {
'dc.description': [
{
value: groupDescription,
},
],
},
}));
})); }));
}); });
describe('with active Group', () => { describe('with active Group', () => {
let expected2; let expected2: Group;
beforeEach(() => { beforeEach(() => {
expected2 = Object.assign(new Group(), { expected2 = Object.assign(new Group(), {
name: 'newGroupName', name: 'newGroupName',
@@ -306,15 +320,24 @@ describe('GroupFormComponent', () => {
}, },
], ],
}, },
object: createSuccessfulRemoteDataObject$(undefined),
_links: {
self: {
href: 'group-selflink',
},
object: {
href: 'group-objectlink',
},
},
}); });
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2)); spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
component.groupName.value = 'newGroupName'; component.ngOnInit();
component.onSubmit();
fixture.detectChanges();
}); });
it('should edit with name and description operations', () => { it('should edit with name and description operations', () => {
component.groupName.setValue('newGroupName');
component.onSubmit();
const operations = [{ const operations = [{
op: 'add', op: 'add',
path: '/metadata/dc.description', path: '/metadata/dc.description',
@@ -328,9 +351,8 @@ describe('GroupFormComponent', () => {
}); });
it('should edit with description operations', () => { it('should edit with description operations', () => {
component.groupName.value = null; component.groupName.setValue(null);
component.onSubmit(); component.onSubmit();
fixture.detectChanges();
const operations = [{ const operations = [{
op: 'add', op: 'add',
path: '/metadata/dc.description', path: '/metadata/dc.description',
@@ -340,9 +362,9 @@ describe('GroupFormComponent', () => {
}); });
it('should edit with name operations', () => { it('should edit with name operations', () => {
component.groupDescription.value = null; component.groupName.setValue('newGroupName');
component.groupDescription.setValue(null);
component.onSubmit(); component.onSubmit();
fixture.detectChanges();
const operations = [{ const operations = [{
op: 'replace', op: 'replace',
path: '/name', path: '/name',
@@ -351,12 +373,13 @@ describe('GroupFormComponent', () => {
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
}); });
it('should emit the existing group using the correct new values', (async () => { it('should emit the existing group using the correct new values', () => {
await fixture.whenStable().then(() => { component.onSubmit();
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
}); });
}));
it('should emit success notification', () => { it('should emit success notification', () => {
component.onSubmit();
expect(notificationService.success).toHaveBeenCalled(); expect(notificationService.success).toHaveBeenCalled();
}); });
}); });
@@ -371,11 +394,8 @@ describe('GroupFormComponent', () => {
describe('check form validation', () => { describe('check form validation', () => {
let groupCommunity;
beforeEach(() => { beforeEach(() => {
groupName = 'testName'; groupName = 'testName';
groupCommunity = 'testgroupCommunity';
groupDescription = 'testgroupDescription'; groupDescription = 'testgroupDescription';
expected = Object.assign(new Group(), { expected = Object.assign(new Group(), {
@@ -387,8 +407,17 @@ describe('GroupFormComponent', () => {
}, },
], ],
}, },
_links: {
self: {
href: 'group-selflink',
},
object: {
href: 'group-objectlink',
},
},
}); });
spyOn(component.submitForm, 'emit'); spyOn(component.submitForm, 'emit');
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
fixture.detectChanges(); fixture.detectChanges();
component.initialisePage(); component.initialisePage();
@@ -438,21 +467,20 @@ describe('GroupFormComponent', () => {
}); });
describe('delete', () => { describe('delete', () => {
let deleteButton; let deleteButton: HTMLButtonElement;
beforeEach(() => { beforeEach(async () => {
component.initialisePage(); spyOn(groupsDataServiceStub, 'delete').and.callThrough();
component.activeGroup$ = observableOf({
component.canEdit$ = observableOf(true); id: 'active-group',
component.groupBeingEdited = {
permanent: false, permanent: false,
} as Group; } as Group);
component.canEdit$ = observableOf(true);
component.initialisePage();
fixture.detectChanges(); fixture.detectChanges();
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; 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', () => { describe('if confirmed via modal', () => {

View File

@@ -11,7 +11,10 @@ import {
OnInit, OnInit,
Output, Output,
} from '@angular/core'; } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms'; import {
AbstractControl,
UntypedFormGroup,
} from '@angular/forms';
import { import {
ActivatedRoute, ActivatedRoute,
Router, Router,
@@ -31,13 +34,10 @@ import { Operation } from 'fast-json-patch';
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
Observable, Observable,
of as observableOf,
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
catchError,
debounceTime, debounceTime,
filter,
map, map,
switchMap, switchMap,
take, take,
@@ -53,7 +53,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
@@ -61,9 +60,9 @@ import { Community } from '../../../core/shared/community.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { import {
getAllCompletedRemoteData,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
@@ -117,9 +116,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
/** /**
* Dynamic models for the inputs of form * Dynamic models for the inputs of form
*/ */
groupName: DynamicInputModel; groupName: AbstractControl;
groupCommunity: DynamicInputModel; groupCommunity: AbstractControl;
groupDescription: DynamicTextAreaModel; groupDescription: AbstractControl;
/** /**
* A list of all dynamic input models * A list of all dynamic input models
@@ -162,21 +161,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
*/ */
subs: Subscription[] = []; 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 * 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<boolean>; canEdit$: Observable<boolean>;
/** /**
* The AlertType enumeration * The current {@link Group}
* @type {AlertType}
*/ */
public AlertTypeEnum = AlertType; activeGroup$: Observable<Group>;
/**
* The current {@link Group}'s linked {@link Community}/{@link Collection}
*/
activeGroupLinkedDSO$: Observable<DSpaceObject>;
/**
* Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab
*/
linkedEditRolesRoute$: Observable<string>;
/**
* The AlertType enumeration
*/
public readonly AlertType = AlertType;
/** /**
* Subscription to email field value change * Subscription to email field value change
@@ -186,126 +194,121 @@ export class GroupFormComponent implements OnInit, OnDestroy {
constructor( constructor(
public groupDataService: GroupDataService, public groupDataService: GroupDataService,
private ePersonDataService: EPersonDataService, protected dSpaceObjectDataService: DSpaceObjectDataService,
private dSpaceObjectDataService: DSpaceObjectDataService, protected formBuilderService: FormBuilderService,
private formBuilderService: FormBuilderService, protected translateService: TranslateService,
private translateService: TranslateService, protected notificationsService: NotificationsService,
private notificationsService: NotificationsService, protected route: ActivatedRoute,
private route: ActivatedRoute,
protected router: Router, protected router: Router,
private authorizationService: AuthorizationDataService, protected authorizationService: AuthorizationDataService,
private modalService: NgbModal, protected modalService: NgbModal,
public requestService: RequestService, public requestService: RequestService,
protected changeDetectorRef: ChangeDetectorRef, protected changeDetectorRef: ChangeDetectorRef,
public dsoNameService: DSONameService, 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(); this.initialisePage();
} }
initialisePage() { initialisePage() {
this.subs.push(this.route.params.subscribe((params) => { const groupNameModel = new DynamicInputModel({
if (params.groupId !== 'newGroup') { id: 'groupName',
this.setActiveGroup(params.groupId); label: this.translateService.instant(`${this.messagePrefix}.groupName`),
} name: 'groupName',
})); validators: {
this.canEdit$ = this.groupDataService.getActiveGroup().pipe( required: null,
hasValueOperator(), },
switchMap((group: Group) => { required: true,
return observableCombineLatest([ });
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), const groupCommunityModel = new DynamicInputModel({
this.hasLinkedDSO(group), id: 'groupCommunity',
]).pipe( label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO), 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();
}
}
}), }),
); );
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);
}
}),
);
});
} }
/** /**
@@ -324,9 +327,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
* Emit the updated/created eperson using the EventEmitter submitForm * Emit the updated/created eperson using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit() {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
(group: Group) => { if (group === null) {
const values = { this.createNewGroup({
name: this.groupName.value, name: this.groupName.value,
metadata: { metadata: {
'dc.description': [ 'dc.description': [
@@ -335,14 +338,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
}, },
], ],
}, },
}; });
if (group === null) { } else {
this.createNewGroup(values); this.editGroup(group);
} else { }
this.editGroup(group); });
}
},
);
} }
/** /**
@@ -448,7 +448,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
* @param groupSelfLink SelfLink of group to set as active * @param groupSelfLink SelfLink of group to set as active
*/ */
setActiveGroupWithLink(groupSelfLink: string) { setActiveGroupWithLink(groupSelfLink: string) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup === null) { if (activeGroup === null) {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object')) this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
@@ -467,7 +467,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. * It'll either show a success or error message depending on whether the delete was successful or not.
*/ */
delete() { delete() {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => { this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
const modalRef = this.modalService.open(ConfirmationModalComponent); const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.name = this.dsoNameService.getName(group); modalRef.componentInstance.name = this.dsoNameService.getName(group);
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
@@ -504,59 +504,45 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
if ( hasValue(this.groupNameValueChangeSubscribe) ) { if (hasValue(this.groupNameValueChangeSubscribe)) {
this.groupNameValueChangeSubscribe.unsubscribe(); this.groupNameValueChangeSubscribe.unsubscribe();
} }
} }
/** /**
* Check if group has a linked object (community or collection linked to a workflow group) * Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
* @param group * workflow group)
*/ */
hasLinkedDSO(group: Group): Observable<boolean> { getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
if (hasValue(group) && hasValue(group._links.object.href)) { return this.activeGroup$.pipe(
return this.getLinkedDSO(group).pipe( hasValueOperator(),
map((rd: RemoteData<DSpaceObject>) => { switchMap((group: Group) => {
return hasValue(rd) && hasValue(rd.payload); if (group.object === undefined) {
}), return this.dSpaceObjectDataService.findByHref(group._links.object.href);
catchError(() => observableOf(false)), }
); return group.object;
} }),
getAllCompletedRemoteData(),
getRemoteDataPayload(),
);
} }
/** /**
* Get group's linked object if it has one (community or collection linked to a workflow group) * Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
* @param group * to a workflow group) if it has one
*/ */
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> { getLinkedEditRolesRoute(): Observable<string> {
if (hasValue(group) && hasValue(group._links.object.href)) { return this.activeGroupLinkedDSO$.pipe(
if (group.object === undefined) { hasValueOperator(),
return this.dSpaceObjectDataService.findByHref(group._links.object.href); map((dso: DSpaceObject) => {
} switch ((dso as any).type) {
return group.object; case Community.type.value:
} return getCommunityEditRolesRoute(dso.id);
} case Collection.type.value:
return getCollectionEditRolesRoute(dso.id);
/** }
* 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<string> {
if (hasValue(group) && hasValue(group._links.object.href)) {
return this.getLinkedDSO(group).pipe(
map((rd: RemoteData<DSpaceObject>) => {
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);
}
}
}),
);
}
} }
} }

View File

@@ -9,9 +9,9 @@
<ds-pagination <ds-pagination
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0" *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
[paginationOptions]="pageConfig" [paginationOptions]="pageConfig"
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements" [collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
[hideGear]="false" [hideGear]="false"
[hidePagerWhenSinglePage]="true"> [hidePagerWhenSinglePage]="true">
<div class="table-responsive"> <div class="table-responsive">
@@ -26,12 +26,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page"> <tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
<td> <td>
<label class="mb-0"> <label class="mb-0">
<input type="checkbox" <input type="checkbox"
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate" [attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
[checked]="isSelected(bitstreamFormat) | async" [checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
(change)="selectBitStreamFormat(bitstreamFormat, $event)" (change)="selectBitStreamFormat(bitstreamFormat, $event)"
> >
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}&#125;</span> <span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}&#125;</span>
@@ -46,13 +46,13 @@
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert"> <div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
{{'admin.registries.bitstream-formats.no-items' | translate}} {{'admin.registries.bitstream-formats.no-items' | translate}}
</div> </div>
<div> <div>
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button> <button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button> <button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,10 +8,7 @@ import { By } from '@angular/platform-browser';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { import { hot } from 'jasmine-marbles';
cold,
hot,
} from 'jasmine-marbles';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
@@ -191,17 +188,17 @@ describe('BitstreamFormatsComponent', () => {
beforeEach(waitForAsync(initAsync)); beforeEach(waitForAsync(initAsync));
beforeEach(initBeforeEach); beforeEach(initBeforeEach);
it('should return an observable of true if the provided bistream is in the list returned by the service', () => { it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
const result = comp.isSelected(bitstreamFormat1); comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
expect(result).toBeObservable(cold('b', { b: true })); });
}); });
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
const format = new BitstreamFormat(); const format = new BitstreamFormat();
format.uuid = 'new'; format.uuid = 'new';
const result = comp.isSelected(format); comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
expect(result).toBeObservable(cold('b', { b: false })); });
}); });
}); });

View File

@@ -13,10 +13,7 @@ import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { import { Observable } from 'rxjs';
combineLatest as observableCombineLatest,
Observable,
} from 'rxjs';
import { import {
map, map,
mergeMap, mergeMap,
@@ -58,7 +55,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
/** /**
* A paginated list of bitstream formats to be shown on the page * A paginated list of bitstream formats to be shown on the page
*/ */
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>; bitstreamFormats$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/**
* The currently selected {@link BitstreamFormat} IDs
*/
selectedBitstreamFormatIDs$: Observable<string[]>;
/** /**
* The current pagination configuration for the page * The current pagination configuration for the page
@@ -125,14 +127,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
} }
/** /**
* Checks whether a given bitstream format is selected in the list (checkbox) * Returns the list of all the bitstream formats that are selected in the list (checkbox)
* @param bitstreamFormat
*/ */
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> { selectedBitstreamFormatIDs(): Observable<string[]> {
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
map((bitstreamFormats: BitstreamFormat[]) => { map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
}),
); );
} }
@@ -156,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
const prefix = 'admin.registries.bitstream-formats.delete'; const prefix = 'admin.registries.bitstream-formats.delete';
const suffix = success ? 'success' : 'failure'; const suffix = success ? 'success' : 'failure';
const messages = observableCombineLatest( const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
this.translateService.get(`${prefix}.${suffix}.head`), const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }),
);
messages.subscribe(([head, content]) => {
if (success) { if (success) {
this.notificationsService.success(head, content); this.notificationsService.success(head, content);
} else { } else {
this.notificationsService.error(head, content); this.notificationsService.error(head, content);
} }
});
} }
ngOnInit(): void { 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) => { switchMap((findListOptions: FindListOptions) => {
return this.bitstreamFormatService.findAll(findListOptions); return this.bitstreamFormatService.findAll(findListOptions);
}), }),
); );
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
} }

View File

@@ -27,14 +27,14 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page" <tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
[ngClass]="{'table-primary' : isActive(schema) | async}"> [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
<td> <td>
<label class="mb-0"> <label class="mb-0">
<input type="checkbox" <input type="checkbox"
[checked]="isSelected(schema) | async" [checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
(change)="selectMetadataSchema(schema, $event)" (change)="selectMetadataSchema(schema, $event)"
> >
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span> <span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
</label> </label>
</td> </td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td> <td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>

View File

@@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service'; import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
import { RestResponse } from '../../../core/cache/response.models';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationService } from '../../../core/pagination/pagination.service';
@@ -36,7 +34,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
import { MetadataRegistryComponent } from './metadata-registry.component'; import { MetadataRegistryComponent } from './metadata-registry.component';
import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component'; import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component';
@@ -44,9 +44,11 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
describe('MetadataRegistryComponent', () => { describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent; let comp: MetadataRegistryComponent;
let fixture: ComponentFixture<MetadataRegistryComponent>; let fixture: ComponentFixture<MetadataRegistryComponent>;
let registryService: RegistryService;
let paginationService; let paginationService: PaginationServiceStub;
const mockSchemasList = [ let registryService: RegistryServiceStub;
const mockSchemasList: MetadataSchema[] = [
{ {
id: 1, id: 1,
_links: { _links: {
@@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => {
prefix: 'mock', prefix: 'mock',
namespace: 'http://dspace.org/mockschema', namespace: 'http://dspace.org/mockschema',
}, },
]; ] as MetadataSchema[];
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();
const configurationDataService = jasmine.createSpyObj('configurationDataService', { const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
@@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => {
); );
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
paginationService = new PaginationServiceStub();
registryService = new RegistryServiceStub();
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => {
EnumKeysPipe, EnumKeysPipe,
], ],
providers: [ providers: [
{ provide: RegistryService, useValue: registryServiceStub }, { provide: RegistryService, useValue: registryService },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ {
@@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
})); }));
it('should cancel editing the selected schema when clicked again', waitForAsync(() => { 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'); spyOn(registryService, 'cancelEditMetadataSchema');
row.click(); row.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); 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(); comp.deleteSchemas();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -7,19 +7,17 @@ import {
import { import {
Component, Component,
OnDestroy, OnDestroy,
OnInit,
} from '@angular/core'; } from '@angular/core';
import { import { RouterLink } from '@angular/router';
Router,
RouterLink,
} from '@angular/router';
import { import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest as observableCombineLatest,
Observable, Observable,
Subscription,
zip, zip,
} from 'rxjs'; } from 'rxjs';
import { import {
@@ -36,7 +34,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
@@ -63,13 +60,23 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
* A component used for managing all existing metadata schemas within the repository. * A component used for managing all existing metadata schemas within the repository.
* The admin can create, edit or delete metadata schemas here. * The admin can create, edit or delete metadata schemas here.
*/ */
export class MetadataRegistryComponent implements OnDestroy { export class MetadataRegistryComponent implements OnDestroy, OnInit {
/** /**
* A list of all the current metadata schemas within the repository * A list of all the current metadata schemas within the repository
*/ */
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>; metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
/**
* The {@link MetadataSchema}that is being edited
*/
activeMetadataSchema$: Observable<MetadataSchema>;
/**
* The selected {@link MetadataSchema} IDs
*/
selectedMetadataSchemaIDs$: Observable<number[]>;
/** /**
* Pagination config used to display the list of metadata schemas * Pagination config used to display the list of metadata schemas
*/ */
@@ -79,15 +86,25 @@ export class MetadataRegistryComponent implements OnDestroy {
}); });
/** /**
* Whether or not the list of MetadataSchemas needs an update * Whether the list of MetadataSchemas needs an update
*/ */
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
constructor(private registryService: RegistryService, subscriptions: Subscription[] = [];
private notificationsService: NotificationsService,
private router: Router, constructor(
private paginationService: PaginationService, protected registryService: RegistryService,
private translateService: TranslateService) { 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(); this.updateSchemas();
} }
@@ -116,30 +133,13 @@ export class MetadataRegistryComponent implements OnDestroy {
* @param schema * @param schema
*/ */
editSchema(schema: MetadataSchema) { editSchema(schema: MetadataSchema) {
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => { this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
if (schema === activeSchema) { if (schema === activeSchema) {
this.registryService.cancelEditMetadataSchema(); this.registryService.cancelEditMetadataSchema();
} else { } else {
this.registryService.editMetadataSchema(schema); this.registryService.editMetadataSchema(schema);
} }
}); }));
}
/**
* Checks whether the given metadata schema is active (being edited)
* @param schema
*/
isActive(schema: MetadataSchema): Observable<boolean> {
return this.getActiveSchema().pipe(
map((activeSchema) => schema === activeSchema),
);
}
/**
* Gets the active metadata schema (being edited)
*/
getActiveSchema(): Observable<MetadataSchema> {
return this.registryService.getActiveMetadataSchema();
} }
/** /**
@@ -153,42 +153,25 @@ export class MetadataRegistryComponent implements OnDestroy {
this.registryService.deselectMetadataSchema(schema); this.registryService.deselectMetadataSchema(schema);
} }
/**
* Checks whether a given metadata schema is selected in the list (checkbox)
* @param schema
*/
isSelected(schema: MetadataSchema): Observable<boolean> {
return this.registryService.getSelectedMetadataSchemas().pipe(
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null),
);
}
/** /**
* Delete all the selected metadata schemas * Delete all the selected metadata schemas
*/ */
deleteSchemas() { deleteSchemas() {
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
(schemas) => { take(1),
const tasks$ = []; switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
for (const schema of schemas) { ).subscribe((responses: RemoteData<NoContent>[]) => {
if (hasValue(schema.id)) { const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
} if (successResponses.length > 0) {
} this.showNotification(true, successResponses.length);
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => { }
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded); if (failedResponses.length > 0) {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed); this.showNotification(false, failedResponses.length);
if (successResponses.length > 0) { }
this.showNotification(true, successResponses.length); this.registryService.deselectAllMetadataSchema();
} this.registryService.cancelEditMetadataSchema();
if (failedResponses.length > 0) { }));
this.showNotification(false, failedResponses.length);
}
this.registryService.deselectAllMetadataSchema();
this.registryService.cancelEditMetadataSchema();
});
},
);
} }
/** /**
@@ -199,20 +182,20 @@ export class MetadataRegistryComponent implements OnDestroy {
showNotification(success: boolean, amount: number) { showNotification(success: boolean, amount: number) {
const prefix = 'admin.registries.schema.notification'; const prefix = 'admin.registries.schema.notification';
const suffix = success ? 'success' : 'failure'; const suffix = success ? 'success' : 'failure';
const messages = observableCombineLatest(
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }), const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount });
);
messages.subscribe(([head, content]) => { if (success) {
if (success) { this.notificationsService.success(head, content);
this.notificationsService.success(head, content); } else {
} else { this.notificationsService.error(head, content);
this.notificationsService.error(head, content); }
}
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.paginationService.clearPagination(this.config.id); this.paginationService.clearPagination(this.config.id);
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
} }
} }

View File

@@ -1,4 +1,4 @@
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div> <div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div>
<ng-template #createHeader> <ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2> <h2>{{messagePrefix + '.create' | translate}}</h2>

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
inject, inject,
@@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../../shared/form/form.component'; import { FormComponent } from '../../../../shared/form/form.component';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
describe('MetadataSchemaFormComponent', () => { describe('MetadataSchemaFormComponent', () => {
let component: MetadataSchemaFormComponent; let component: MetadataSchemaFormComponent;
let fixture: ComponentFixture<MetadataSchemaFormComponent>; let fixture: ComponentFixture<MetadataSchemaFormComponent>;
let registryService: RegistryService;
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */ let registryService: RegistryServiceStub;
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 */
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
registryService = new RegistryServiceStub();
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
providers: [ providers: [
{ provide: RegistryService, useValue: registryServiceStub }, { provide: RegistryService, useValue: registryService },
{ provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: FormBuilderService, useValue: getMockFormBuilderService() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.overrideComponent(MetadataSchemaFormComponent, { .overrideComponent(MetadataSchemaFormComponent, {
remove: { remove: {
@@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
describe('without an active schema', () => { describe('without an active schema', () => {
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined)); component.activeMetadataSchema$ = observableOf(undefined);
component.onSubmit(); component.onSubmit();
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
} as MetadataSchema); } as MetadataSchema);
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); component.activeMetadataSchema$ = observableOf(expectedWithId);
component.onSubmit(); component.onSubmit();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -21,13 +21,13 @@ import {
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { import {
combineLatest,
Observable, Observable,
Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
map,
switchMap, switchMap,
take, take,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
@@ -102,64 +102,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
*/ */
@Output() submitForm: EventEmitter<any> = new EventEmitter(); @Output() submitForm: EventEmitter<any> = new EventEmitter();
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { /**
* The {@link MetadataSchema} that is currently being edited
*/
activeMetadataSchema$: Observable<MetadataSchema>;
subscriptions: Subscription[] = [];
constructor(
protected registryService: RegistryService,
protected formBuilderService: FormBuilderService,
protected translateService: TranslateService,
) {
} }
ngOnInit() { ngOnInit() {
combineLatest([ this.name = new DynamicInputModel({
this.translateService.get(`${this.messagePrefix}.name`), id: 'name',
this.translateService.get(`${this.messagePrefix}.namespace`), label: this.translateService.instant(`${this.messagePrefix}.name`),
]).subscribe(([name, namespace]) => { name: 'name',
this.name = new DynamicInputModel({ validators: {
id: 'name', required: null,
label: name, pattern: '^[^. ,]*$',
name: 'name', maxLength: 32,
validators: { },
required: null, required: true,
pattern: '^[^. ,]*$', errorMessages: {
maxLength: 32, pattern: 'error.validation.metadata.name.invalid-pattern',
}, maxLength: 'error.validation.metadata.name.max-length',
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.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;
}
}));
} }
/** /**
@@ -176,48 +183,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
* Emit the updated/created schema using the EventEmitter submitForm * Emit the updated/created schema using the EventEmitter submitForm
*/ */
onSubmit(): void { onSubmit(): void {
this.registryService this.activeMetadataSchema$.pipe(
.getActiveMetadataSchema() take(1),
.pipe( switchMap((schema: MetadataSchema) => {
take(1), const metadataValues = {
switchMap((schema: MetadataSchema) => { prefix: this.name.value,
const metadataValues = { namespace: this.namespace.value,
prefix: this.name.value, };
namespace: this.namespace.value, if (schema == null) {
}; return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
} else {
let createOrUpdate$: Observable<MetadataSchema>; return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
namespace: metadataValues.namespace,
if (schema == null) { }));
createOrUpdate$ = }
this.registryService.createOrUpdateMetadataSchema( }),
Object.assign(new MetadataSchema(), metadataValues), switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
); map(() => updatedOrCreatedSchema),
} else { )),
const updatedSchema = Object.assign( ).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
new MetadataSchema(), this.submitForm.emit(updatedOrCreatedSchema);
schema, this.clearFields();
{ this.registryService.cancelEditMetadataSchema();
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();
});
} }
/** /**
@@ -233,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.onCancel(); this.onCancel();
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
} }
} }

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
inject, inject,
@@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { FormComponent } from '../../../../shared/form/form.component'; import { FormComponent } from '../../../../shared/form/form.component';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
import { MetadataFieldFormComponent } from './metadata-field-form.component'; import { MetadataFieldFormComponent } from './metadata-field-form.component';
describe('MetadataFieldFormComponent', () => { describe('MetadataFieldFormComponent', () => {
let component: MetadataFieldFormComponent; let component: MetadataFieldFormComponent;
let fixture: ComponentFixture<MetadataFieldFormComponent>; let fixture: ComponentFixture<MetadataFieldFormComponent>;
let registryService: RegistryService;
let registryService: RegistryServiceStub;
const metadataSchema = Object.assign(new MetadataSchema(), { const metadataSchema = Object.assign(new MetadataSchema(), {
id: 1, id: 1,
@@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => {
prefix: 'fake', 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(() => { beforeEach(waitForAsync(() => {
registryService = new RegistryServiceStub();
return TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
providers: [ providers: [
{ provide: RegistryService, useValue: registryServiceStub }, { provide: RegistryService, useValue: registryService },
{ provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: FormBuilderService, useValue: getMockFormBuilderService() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.overrideComponent(MetadataFieldFormComponent, { .overrideComponent(MetadataFieldFormComponent, {
remove: { imports: [FormComponent] }, remove: { imports: [FormComponent] },

View File

@@ -31,8 +31,8 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let field of fields?.page" <tr *ngFor="let field of fields?.page"
[ngClass]="{'table-primary' : isActive(field) | async}"> [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
<td *ngVar="(isSelected(field) | async) as selected"> <td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
<input type="checkbox" <input type="checkbox"
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate" [attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
[checked]="selected" [checked]="selected"

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { import {
ComponentFixture, ComponentFixture,
inject, inject,
@@ -7,16 +7,12 @@ import {
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { import { ActivatedRoute } from '@angular/router';
ActivatedRoute,
Router,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { RestResponse } from '../../../core/cache/response.models';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
@@ -34,7 +30,7 @@ import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub'; import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
@@ -45,8 +41,12 @@ import { MetadataSchemaComponent } from './metadata-schema.component';
describe('MetadataSchemaComponent', () => { describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent; let comp: MetadataSchemaComponent;
let fixture: ComponentFixture<MetadataSchemaComponent>; let fixture: ComponentFixture<MetadataSchemaComponent>;
let registryService: RegistryService;
const mockSchemasList = [ let registryService: RegistryServiceStub;
let activatedRoute: ActivatedRouteStub;
let paginationService: PaginationServiceStub;
const mockSchemasList: MetadataSchema[] = [
{ {
id: 1, id: 1,
_links: { _links: {
@@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => {
prefix: 'mock', prefix: 'mock',
namespace: 'http://dspace.org/mockschema', namespace: 'http://dspace.org/mockschema',
}, },
]; ] as MetadataSchema[];
const mockFieldsList = [ const mockFieldsList: MetadataField[] = [
{ {
id: 1, id: 1,
_links: { _links: {
@@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => {
scopeNote: null, scopeNote: null,
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
}, },
]; ] as MetadataField[];
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 */
const schemaNameParam = 'mock'; const schemaNameParam = 'mock';
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: observableOf({
schemaName: schemaNameParam,
}),
});
const paginationService = new PaginationServiceStub();
const configurationDataService = jasmine.createSpyObj('configurationDataService', { const configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
@@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => {
beforeEach(waitForAsync(() => { 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({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => {
VarDirective, VarDirective,
], ],
providers: [ providers: [
{ provide: RegistryService, useValue: registryServiceStub }, { provide: RegistryService, useValue: registryService },
{ provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: Router, useValue: new RouterStub() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ {
provide: NotificationsService, provide: NotificationsService,
@@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => {
{ provide: ConfigurationDataService, useValue: configurationDataService }, { provide: ConfigurationDataService, useValue: configurationDataService },
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
], ],
schemas: [NO_ERRORS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
.overrideComponent(MetadataSchemaComponent, { .overrideComponent(MetadataSchemaComponent, {
remove: { remove: {
@@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => {
})); }));
it('should cancel editing the selected field when clicked again', waitForAsync(() => { 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'); spyOn(registryService, 'cancelEditMetadataField');
row.click(); row.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'deleteMetadataField').and.callThrough(); 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(); comp.deleteFields();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -20,9 +20,9 @@ import {
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
combineLatest as observableCombineLatest,
Observable, Observable,
of as observableOf, of as observableOf,
Subscription,
zip, zip,
} from 'rxjs'; } from 'rxjs';
import { import {
@@ -42,7 +42,6 @@ import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
@@ -71,7 +70,7 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
* A component used for managing all existing metadata fields within the current metadata schema. * A component used for managing all existing metadata fields within the current metadata schema.
* The admin can create, edit or delete metadata fields here. * 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 * The metadata schema
*/ */
@@ -96,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
*/ */
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true); needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
constructor(private registryService: RegistryService, /**
private route: ActivatedRoute, * The current {@link MetadataField} that is being edited
private notificationsService: NotificationsService, */
private paginationService: PaginationService, activeField$: Observable<MetadataField>;
private translateService: TranslateService) {
/**
* The selected {@link MetadataField} IDs
*/
selectedMetadataFieldIDs$: Observable<number[]>;
subscriptions: Subscription[] = [];
constructor(
protected registryService: RegistryService,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected paginationService: PaginationService,
protected translateService: TranslateService,
) {
} }
ngOnInit(): void { ngOnInit(): void {
this.route.params.subscribe((params) => { this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
this.initialize(params); this.activeField$ = this.registryService.getActiveMetadataField();
}); this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
} map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
);
/**
* Initialize the component using the params within the url (schemaName)
* @param params
*/
initialize(params) {
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
this.updateFields(); this.updateFields();
} }
@@ -148,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
* @param field * @param field
*/ */
editField(field: MetadataField) { editField(field: MetadataField) {
this.getActiveField().pipe(take(1)).subscribe((activeField) => { this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
if (field === activeField) { if (field === activeField) {
this.registryService.cancelEditMetadataField(); this.registryService.cancelEditMetadataField();
} else { } else {
this.registryService.editMetadataField(field); this.registryService.editMetadataField(field);
} }
}); }));
}
/**
* Checks whether the given metadata field is active (being edited)
* @param field
*/
isActive(field: MetadataField): Observable<boolean> {
return this.getActiveField().pipe(
map((activeField) => field === activeField),
);
}
/**
* Gets the active metadata field (being edited)
*/
getActiveField(): Observable<MetadataField> {
return this.registryService.getActiveMetadataField();
} }
/** /**
@@ -185,42 +174,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
this.registryService.deselectMetadataField(field); this.registryService.deselectMetadataField(field);
} }
/**
* Checks whether a given metadata field is selected in the list (checkbox)
* @param field
*/
isSelected(field: MetadataField): Observable<boolean> {
return this.registryService.getSelectedMetadataFields().pipe(
map((fields) => fields.find((selectedField) => selectedField === field) != null),
);
}
/** /**
* Delete all the selected metadata fields * Delete all the selected metadata fields
*/ */
deleteFields() { deleteFields() {
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
(fields) => { take(1),
const tasks$ = []; switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
for (const field of fields) { ).subscribe((responses: RemoteData<NoContent>[]) => {
if (hasValue(field.id)) { const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
} if (successResponses.length > 0) {
} this.showNotification(true, successResponses.length);
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => { }
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded); if (failedResponses.length > 0) {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed); this.showNotification(false, failedResponses.length);
if (successResponses.length > 0) { }
this.showNotification(true, successResponses.length); this.registryService.deselectAllMetadataField();
} this.registryService.cancelEditMetadataField();
if (failedResponses.length > 0) { }));
this.showNotification(false, failedResponses.length);
}
this.registryService.deselectAllMetadataField();
this.registryService.cancelEditMetadataField();
});
},
);
} }
/** /**
@@ -231,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
showNotification(success: boolean, amount: number) { showNotification(success: boolean, amount: number) {
const prefix = 'admin.registries.schema.notification'; const prefix = 'admin.registries.schema.notification';
const suffix = success ? 'success' : 'failure'; const suffix = success ? 'success' : 'failure';
const messages = observableCombineLatest([ const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }), if (success) {
]); this.notificationsService.success(head, content);
messages.subscribe(([head, content]) => { } else {
if (success) { this.notificationsService.error(head, content);
this.notificationsService.success(head, content); }
} else {
this.notificationsService.error(head, content);
}
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.paginationService.clearPagination(this.config.id); this.paginationService.clearPagination(this.config.id);
this.registryService.deselectAllMetadataField(); this.registryService.deselectAllMetadataField();
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
} }
} }

View File

@@ -8,7 +8,7 @@
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span> <span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
<div class="gap-2 d-flex"> <div class="gap-2 d-flex">
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a>
<a *ngIf="showReinstateButton$() | async" class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a> <a *ngIf="showReinstateButton$ | async" class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
</div> </div>
</div> </div>
</ds-alert> </ds-alert>

View File

@@ -161,7 +161,7 @@ describe('ItemAlertsComponent', () => {
(authorizationService.isAuthorized).and.returnValue(isAdmin$); (authorizationService.isAuthorized).and.returnValue(isAdmin$);
(correctionTypeDataService.findByItem).and.returnValue(correction$); (correctionTypeDataService.findByItem).and.returnValue(correction$);
expectObservable(component.showReinstateButton$()).toBe(expectedMarble, expectedValues); expectObservable(component.shouldShowReinstateButton()).toBe(expectedMarble, expectedValues);
}); });
}); });

View File

@@ -5,6 +5,8 @@ import {
import { import {
Component, Component,
Input, Input,
OnChanges,
SimpleChanges,
} from '@angular/core'; } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -45,12 +47,17 @@ import {
/** /**
* Component displaying alerts for an item * Component displaying alerts for an item
*/ */
export class ItemAlertsComponent { export class ItemAlertsComponent implements OnChanges {
/** /**
* The Item to display alerts for * The Item to display alerts for
*/ */
@Input() item: Item; @Input() item: Item;
/**
* Whether the reinstate button should be shown
*/
showReinstateButton$: Observable<boolean>;
/** /**
* The AlertType enumeration * The AlertType enumeration
* @type {AlertType} * @type {AlertType}
@@ -58,18 +65,24 @@ export class ItemAlertsComponent {
public AlertTypeEnum = AlertType; public AlertTypeEnum = AlertType;
constructor( constructor(
private authService: AuthorizationDataService, protected authService: AuthorizationDataService,
private dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService,
private correctionTypeDataService: CorrectionTypeDataService, protected correctionTypeDataService: CorrectionTypeDataService,
) { ) {
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes.item?.currentValue.withdrawn && this.showReinstateButton$) {
this.showReinstateButton$ = this.shouldShowReinstateButton();
}
}
/** /**
* Determines whether to show the reinstate button. * Determines whether to show the reinstate button.
* The button is shown if the user is not an admin and the item has a reinstate request. * The button is shown if the user is not an admin and the item has a reinstate request.
* @returns An Observable that emits a boolean value indicating whether to show the reinstate button. * @returns An Observable that emits a boolean value indicating whether to show the reinstate button.
*/ */
showReinstateButton$(): Observable<boolean> { shouldShowReinstateButton(): Observable<boolean> {
const correction$ = this.correctionTypeDataService.findByItem(this.item.uuid, true).pipe( const correction$ = this.correctionTypeDataService.findByItem(this.item.uuid, true).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((correctionTypeRD: RemoteData<PaginatedList<CorrectionType>>) => correctionTypeRD.hasSucceeded ? correctionTypeRD.payload.page : []), map((correctionTypeRD: RemoteData<PaginatedList<CorrectionType>>) => correctionTypeRD.hasSucceeded ? correctionTypeRD.payload.page : []),
@@ -78,8 +91,8 @@ export class ItemAlertsComponent {
return combineLatest([isAdmin$, correction$]).pipe( return combineLatest([isAdmin$, correction$]).pipe(
map(([isAdmin, correction]) => { map(([isAdmin, correction]) => {
return !isAdmin && correction.some((correctionType) => correctionType.topic === REQUEST_REINSTATE); return !isAdmin && correction.some((correctionType) => correctionType.topic === REQUEST_REINSTATE);
}, }),
)); );
} }
/** /**

View File

@@ -1,7 +1,7 @@
<ng-container *ngVar="(user$ | async) as user"> <ng-container *ngVar="(user$ | async) as user">
<div class="container" *ngIf="user"> <div class="container" *ngIf="user">
<h1>{{'profile.title' | translate}}</h1> <h1>{{'profile.title' | translate}}</h1>
<ng-container> <ng-container *ngIf="isResearcherProfileEnabled$ | async">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">{{'profile.card.researcher' | translate}}</div> <div class="card-header">{{'profile.card.researcher' | translate}}</div>
<div class="card-body"> <div class="card-body">

View File

@@ -369,7 +369,7 @@ describe('ProfilePageComponent', () => {
}); });
it('should return true', () => { it('should return true', () => {
const result = component.isResearcherProfileEnabled(); const result = component.isResearcherProfileEnabled$;
const expected = cold('a', { const expected = cold('a', {
a: true, a: true,
}); });
@@ -385,7 +385,7 @@ describe('ProfilePageComponent', () => {
}); });
it('should return false', () => { it('should return false', () => {
const result = component.isResearcherProfileEnabled(); const result = component.isResearcherProfileEnabled$;
const expected = cold('a', { const expected = cold('a', {
a: false, a: false,
}); });
@@ -401,7 +401,7 @@ describe('ProfilePageComponent', () => {
}); });
it('should return false', () => { it('should return false', () => {
const result = component.isResearcherProfileEnabled(); const result = component.isResearcherProfileEnabled$;
const expected = cold('a', { const expected = cold('a', {
a: false, a: false,
}); });

View File

@@ -235,13 +235,6 @@ export class ProfilePageComponent implements OnInit {
this.updateProfile(); this.updateProfile();
} }
/**
* Returns true if the researcher profile feature is enabled, false otherwise.
*/
isResearcherProfileEnabled(): Observable<boolean> {
return this.isResearcherProfileEnabled$.asObservable();
}
/** /**
* Returns an error message from a password validation request with a specific reason or * Returns an error message from a password validation request with a specific reason or
* a default message without specific reason. * a default message without specific reason.

View File

@@ -8,7 +8,7 @@
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="item?.thumbnail | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="item?.thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ng-container *ngVar="(getFiles() | async) as bitstreams"> <ng-container *ngIf="(bitstreams$ | async) as bitstreams">
<ds-metadata-field-wrapper [label]="('item.page.files' | translate)"> <ds-metadata-field-wrapper [label]="('item.page.files' | translate)">
<div *ngIf="bitstreams?.length > 0" class="file-section"> <div *ngIf="bitstreams?.length > 0" class="file-section">
<button class="btn btn-link" *ngFor="let file of bitstreams; let last=last;" (click)="downloadBitstreamFile(file?.uuid)"> <button class="btn btn-link" *ngFor="let file of bitstreams; let last=last;" (click)="downloadBitstreamFile(file?.uuid)">

View File

@@ -6,6 +6,8 @@ import {
import { import {
Component, Component,
Input, Input,
OnChanges,
SimpleChanges,
} from '@angular/core'; } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -22,6 +24,7 @@ import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/oper
import { ThemedItemPageTitleFieldComponent } from '../../../../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../../../../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component'; import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
import { fadeInOut } from '../../../animations/fade'; import { fadeInOut } from '../../../animations/fade';
import { hasValue } from '../../../empty.util';
import { MetadataFieldWrapperComponent } from '../../../metadata-field-wrapper/metadata-field-wrapper.component'; import { MetadataFieldWrapperComponent } from '../../../metadata-field-wrapper/metadata-field-wrapper.component';
import { ThemedBadgesComponent } from '../../../object-collection/shared/badges/themed-badges.component'; import { ThemedBadgesComponent } from '../../../object-collection/shared/badges/themed-badges.component';
import { ItemSubmitterComponent } from '../../../object-collection/shared/mydspace-item-submitter/item-submitter.component'; import { ItemSubmitterComponent } from '../../../object-collection/shared/mydspace-item-submitter/item-submitter.component';
@@ -41,7 +44,7 @@ import { ThemedItemDetailPreviewFieldComponent } from './item-detail-preview-fie
standalone: true, standalone: true,
imports: [NgIf, ThemedBadgesComponent, ThemedItemPageTitleFieldComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, VarDirective, NgFor, ThemedItemDetailPreviewFieldComponent, ItemSubmitterComponent, AsyncPipe, FileSizePipe, TranslateModule], imports: [NgIf, ThemedBadgesComponent, ThemedItemPageTitleFieldComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, VarDirective, NgFor, ThemedItemDetailPreviewFieldComponent, ItemSubmitterComponent, AsyncPipe, FileSizePipe, TranslateModule],
}) })
export class ItemDetailPreviewComponent { export class ItemDetailPreviewComponent implements OnChanges {
/** /**
* The item to display * The item to display
*/ */
@@ -80,6 +83,12 @@ export class ItemDetailPreviewComponent {
) { ) {
} }
ngOnChanges(changes: SimpleChanges): void {
if (hasValue(changes.item)) {
this.bitstreams$ = this.getFiles();
}
}
/** /**
* Perform bitstream download * Perform bitstream download
*/ */

View File

@@ -3,7 +3,7 @@
<div class="row"> <div class="row">
<div *ngIf="!hidePaginationDetail && collectionSize > 0" class="col-auto pagination-info"> <div *ngIf="!hidePaginationDetail && collectionSize > 0" class="col-auto pagination-info">
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span> <span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
<span class="align-middle">{{ 'pagination.showing.detail' | translate:(getShowingDetails(collectionSize)|async)}}</span> <span class="align-middle">{{ 'pagination.showing.detail' | translate:(showingDetails$ | async)}}</span>
</div> </div>
<div class="col"> <div class="col">
<div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right"> <div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">

View File

@@ -10,9 +10,11 @@ import {
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
SimpleChanges,
ViewEncapsulation, ViewEncapsulation,
} from '@angular/core'; } from '@angular/core';
import { import {
@@ -28,6 +30,8 @@ import {
} from 'rxjs'; } from 'rxjs';
import { import {
map, map,
startWith,
switchMap,
take, take,
} from 'rxjs/operators'; } from 'rxjs/operators';
@@ -40,13 +44,21 @@ import { RemoteData } from '../../core/data/remote-data';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface'; import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { hasValue } from '../empty.util'; import {
hasValue,
hasValueOperator,
} from '../empty.util';
import { HostWindowService } from '../host-window.service'; import { HostWindowService } from '../host-window.service';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
import { RSSComponent } from '../rss-feed/rss.component'; import { RSSComponent } from '../rss-feed/rss.component';
import { EnumKeysPipe } from '../utils/enum-keys-pipe'; import { EnumKeysPipe } from '../utils/enum-keys-pipe';
import { PaginationComponentOptions } from './pagination-component-options.model'; import { PaginationComponentOptions } from './pagination-component-options.model';
interface PaginationDetails {
range: string;
total: number;
}
/** /**
* The default pagination controls component. * The default pagination controls component.
*/ */
@@ -60,7 +72,7 @@ import { PaginationComponentOptions } from './pagination-component-options.model
standalone: true, standalone: true,
imports: [NgIf, NgbDropdownModule, NgFor, NgClass, RSSComponent, NgbPaginationModule, NgbTooltipModule, AsyncPipe, TranslateModule, EnumKeysPipe], imports: [NgIf, NgbDropdownModule, NgFor, NgClass, RSSComponent, NgbPaginationModule, NgbTooltipModule, AsyncPipe, TranslateModule, EnumKeysPipe],
}) })
export class PaginationComponent implements OnDestroy, OnInit { export class PaginationComponent implements OnChanges, OnDestroy, OnInit {
/** /**
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}. * ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
*/ */
@@ -205,6 +217,9 @@ export class PaginationComponent implements OnDestroy, OnInit {
public sortField$: Observable<string>; public sortField$: Observable<string>;
public defaultSortField = 'name'; public defaultSortField = 'name';
public showingDetails$: Observable<PaginationDetails>;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array} * @type {Array}
@@ -233,6 +248,12 @@ export class PaginationComponent implements OnDestroy, OnInit {
this.initializeConfig(); this.initializeConfig();
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes.collectionSize.currentValue !== changes.collectionSize.previousValue) {
this.showingDetails$ = this.getShowingDetails(this.collectionSize);
}
}
/** /**
* Method provided by Angular. Invoked when the instance is destroyed. * Method provided by Angular. Invoked when the instance is destroyed.
*/ */
@@ -270,9 +291,11 @@ export class PaginationComponent implements OnDestroy, OnInit {
); );
} }
constructor(private cdRef: ChangeDetectorRef, constructor(
private paginationService: PaginationService, protected cdRef: ChangeDetectorRef,
public hostWindowService: HostWindowService) { protected paginationService: PaginationService,
public hostWindowService: HostWindowService,
) {
} }
/** /**
@@ -326,25 +349,30 @@ export class PaginationComponent implements OnDestroy, OnInit {
/** /**
* Method to get pagination details of the current viewed page. * Method to get pagination details of the current viewed page.
*/ */
public getShowingDetails(collectionSize: number): Observable<any> { public getShowingDetails(collectionSize: number): Observable<PaginationDetails> {
let showingDetails = observableOf({ range: null + ' - ' + null, total: null }); return observableOf(collectionSize).pipe(
if (collectionSize) { hasValueOperator(),
showingDetails = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( switchMap(() => this.paginationService.getCurrentPagination(this.id, this.paginationOptions)),
map((currentPaginationOptions) => { map((currentPaginationOptions) => {
let lastItem: number; let lastItem: number;
const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage; const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage;
const firstItem: number = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1; const firstItem: number = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1;
if (collectionSize > pageMax) { if (collectionSize > pageMax) {
lastItem = pageMax; lastItem = pageMax;
} else { } else {
lastItem = collectionSize; lastItem = collectionSize;
} }
return { range: firstItem + ' - ' + lastItem, total: collectionSize }; return {
}), range: `${firstItem} - ${lastItem}`,
); total: collectionSize,
} };
return showingDetails; }),
startWith({
range: `${null} - ${null}`,
total: null,
}),
);
} }
/** /**

View File

@@ -0,0 +1,111 @@
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
import {
Observable,
of as observableOf,
} from 'rxjs';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { MetadataField } from '../../core/metadata/metadata-field.model';
import { MetadataSchema } from '../../core/metadata/metadata-schema.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { FollowLinkConfig } from '../utils/follow-link-config.model';
import { createPaginatedList } from './utils.test';
/**
* Stub class of {@link RegistryService}
*/
export class RegistryServiceStub {
getMetadataSchemas(_options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<MetadataSchema>[]): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
return createSuccessfulRemoteDataObject$(createPaginatedList());
}
getMetadataSchemaByPrefix(_prefix: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<MetadataSchema>[]): Observable<RemoteData<MetadataSchema>> {
return createSuccessfulRemoteDataObject$(undefined);
}
getMetadataFieldsBySchema(_schema: MetadataSchema, _options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> {
return createSuccessfulRemoteDataObject$(createPaginatedList());
}
editMetadataSchema(_schema: MetadataSchema): void {
}
cancelEditMetadataSchema(): void {
}
getActiveMetadataSchema(): Observable<MetadataSchema> {
return observableOf(undefined);
}
selectMetadataSchema(_schema: MetadataSchema): void {
}
deselectMetadataSchema(_schema: MetadataSchema): void {
}
deselectAllMetadataSchema(): void {
}
getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
return observableOf([]);
}
editMetadataField(_field: MetadataField): void {
}
cancelEditMetadataField(): void {
}
getActiveMetadataField(): Observable<MetadataField> {
return observableOf(undefined);
}
selectMetadataField(_field: MetadataField): void {
}
deselectMetadataField(_field: MetadataField): void {
}
deselectAllMetadataField(): void {
}
getSelectedMetadataFields(): Observable<MetadataField[]> {
return observableOf([]);
}
createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
return observableOf(schema);
}
deleteMetadataSchema(_id: number): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$(undefined);
}
clearMetadataSchemaRequests(): Observable<string> {
return observableOf('');
}
createMetadataField(field: MetadataField, _schema: MetadataSchema): Observable<MetadataField> {
return observableOf(field);
}
updateMetadataField(field: MetadataField): Observable<MetadataField> {
return observableOf(field);
}
deleteMetadataField(_id: number): Observable<RemoteData<NoContent>> {
return createSuccessfulRemoteDataObject$(undefined);
}
clearMetadataFieldRequests(): void {
}
queryMetadataFields(_query: string, _options: FindListOptions = {}, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<MetadataField>[]): Observable<RemoteData<PaginatedList<MetadataField>>> {
return createSuccessfulRemoteDataObject$(createPaginatedList());
}
}