forked from hazza/dspace-angular
Merge branch 'w2p-117573_remove-observable-function-calls-from-template-7.6'
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
|
@@ -100,6 +100,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -165,6 +167,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
initialisePage() {
|
||||
this.searching$.next(true);
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
this.subs.push(this.ePeople$.pipe(
|
||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||
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
|
||||
*/
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="group-form row">
|
||||
<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>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<div *ngIf="activeEPerson$ | async">
|
||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -19,12 +19,10 @@ import {
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
RouterModule,
|
||||
} from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
} from '@ngx-translate/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
@@ -49,7 +47,6 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se
|
||||
import { FormComponent } from '../../../shared/form/form.component';
|
||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||
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 { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
@@ -92,9 +89,6 @@ describe('EPersonFormComponent', () => {
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
@@ -228,12 +222,8 @@ describe('EPersonFormComponent', () => {
|
||||
router = new RouterStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
RouterModule.forRoot([]),
|
||||
TranslateModule.forRoot(),
|
||||
EPersonFormComponent,
|
||||
HasNoValuePipe,
|
||||
],
|
||||
@@ -251,7 +241,7 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: Router, useValue: router },
|
||||
EPeopleRegistryComponent,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(EPersonFormComponent, {
|
||||
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
||||
@@ -274,37 +264,13 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('check form validation', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let requireCertificate;
|
||||
let canLogIn: boolean;
|
||||
let requireCertificate: boolean;
|
||||
|
||||
let expected;
|
||||
beforeEach(() => {
|
||||
firstName = 'testName';
|
||||
lastName = 'testLastName';
|
||||
email = 'testEmail@test.com';
|
||||
canLogIn = false;
|
||||
requireCertificate = false;
|
||||
|
||||
expected = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: firstName,
|
||||
},
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: lastName,
|
||||
},
|
||||
],
|
||||
},
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.canLogIn.value = canLogIn;
|
||||
component.requireCertificate.value = requireCertificate;
|
||||
@@ -378,15 +344,13 @@ describe('EPersonFormComponent', () => {
|
||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let canLogIn: boolean;
|
||||
let requireCertificate;
|
||||
|
||||
let expected;
|
||||
@@ -415,6 +379,7 @@ describe('EPersonFormComponent', () => {
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.ngOnInit();
|
||||
component.firstName.value = firstName;
|
||||
component.lastName.value = lastName;
|
||||
component.email.value = email;
|
||||
@@ -454,9 +419,17 @@ describe('EPersonFormComponent', () => {
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
_links: undefined,
|
||||
_links: {
|
||||
groups: {
|
||||
href: '',
|
||||
},
|
||||
self: {
|
||||
href: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||
component.ngOnInit();
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -504,22 +477,19 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
||||
let ePersonId;
|
||||
let eperson: EPerson;
|
||||
let modalService;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
ePersonId = 'testEPersonId';
|
||||
eperson = EPersonMock;
|
||||
component.epersonInitial = eperson;
|
||||
component.canDelete$ = observableOf(true);
|
||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
});
|
||||
|
||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||
|
@@ -189,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
canImpersonate$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The current {@link EPerson}
|
||||
*/
|
||||
activeEPerson$: Observable<EPerson>;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
@@ -254,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
if (hasValue(eperson)) {
|
||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||
@@ -262,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.submitLabel = 'form.submit';
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
@@ -275,17 +281,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
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,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.firstName`),
|
||||
name: 'firstName',
|
||||
validators: {
|
||||
required: null,
|
||||
@@ -294,7 +292,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.lastName = new DynamicInputModel({
|
||||
id: 'lastName',
|
||||
label: lastName,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.lastName`),
|
||||
name: 'lastName',
|
||||
validators: {
|
||||
required: null,
|
||||
@@ -303,7 +301,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.email = new DynamicInputModel({
|
||||
id: 'email',
|
||||
label: email,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.email`),
|
||||
name: 'email',
|
||||
validators: {
|
||||
required: null,
|
||||
@@ -314,19 +312,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
emailTaken: 'error.validation.emailTaken',
|
||||
pattern: 'error.validation.NotValidEmail',
|
||||
},
|
||||
hint: emailHint,
|
||||
hint: this.translateService.instant(`${this.messagePrefix}.emailHint`),
|
||||
});
|
||||
this.canLogIn = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: 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: requireCertificate,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`),
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false),
|
||||
});
|
||||
@@ -338,7 +336,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.requireCertificate,
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||
if (eperson != null) {
|
||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||
currentPage: 1,
|
||||
@@ -361,9 +359,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}));
|
||||
|
||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
|
||||
this.groups$ = activeEPerson$.pipe(
|
||||
this.groups$ = this.activeEPerson$.pipe(
|
||||
switchMap((eperson) => {
|
||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||
currentPage: 1,
|
||||
@@ -382,7 +378,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
map(groupsRD => groupsRD.payload.pageInfo),
|
||||
);
|
||||
|
||||
this.canImpersonate$ = activeEPerson$.pipe(
|
||||
this.canImpersonate$ = this.activeEPerson$.pipe(
|
||||
switchMap((eperson) => {
|
||||
if (hasValue(eperson)) {
|
||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||
@@ -391,11 +387,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.canDelete$ = activeEPerson$.pipe(
|
||||
this.canDelete$ = this.activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
||||
);
|
||||
this.canReset$ = observableOf(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,7 +409,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
||||
this.activeEPerson$.pipe(take(1)).subscribe(
|
||||
(ePerson: EPerson) => {
|
||||
const values = {
|
||||
metadata: {
|
||||
@@ -533,7 +528,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete(): void {
|
||||
this.epersonService.getActiveEPerson().pipe(
|
||||
this.activeEPerson$.pipe(
|
||||
take(1),
|
||||
switchMap((eperson: EPerson) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
@@ -637,7 +632,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* Update the list of groups by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateGroups(options) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||
}));
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="group-form row">
|
||||
<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>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||
@@ -23,11 +23,15 @@
|
||||
</h1>
|
||||
</ng-template>
|
||||
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
|
||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
||||
[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 *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"
|
||||
[formModel]="formModel"
|
||||
@@ -39,22 +43,21 @@
|
||||
<button (click)="onCancel()" type="button"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</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">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ds-form>
|
||||
|
||||
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||
<div class="mb-5">
|
||||
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
</div>
|
||||
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
|
||||
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -23,11 +23,7 @@ import {
|
||||
} from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
Observable,
|
||||
@@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component';
|
||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import {
|
||||
GroupMock,
|
||||
GroupMock2,
|
||||
} from '../../../shared/testing/group-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
||||
import { GroupFormComponent } from './group-form.component';
|
||||
import { MembersListComponent } from './members-list/members-list.component';
|
||||
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
||||
@@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
let fixture: ComponentFixture<GroupFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let dsoDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let router;
|
||||
let router: RouterMock;
|
||||
let route: ActivatedRouteStub;
|
||||
|
||||
let groups;
|
||||
let groupName;
|
||||
let groupDescription;
|
||||
let expected;
|
||||
let groups: Group[];
|
||||
let groupName: string;
|
||||
let groupDescription: string;
|
||||
let expected: Group;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
groups = [GroupMock, GroupMock2];
|
||||
@@ -105,6 +100,15 @@ describe('GroupFormComponent', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
object: createSuccessfulRemoteDataObject$(undefined),
|
||||
_links: {
|
||||
self: {
|
||||
href: 'group-selflink',
|
||||
},
|
||||
object: {
|
||||
href: 'group-objectlink',
|
||||
},
|
||||
},
|
||||
});
|
||||
ePersonDataServiceStub = {};
|
||||
groupsDataServiceStub = {
|
||||
@@ -141,7 +145,14 @@ describe('GroupFormComponent', () => {
|
||||
create(group: Group): Observable<RemoteData<Group>> {
|
||||
this.allGroups = [...this.allGroups, group];
|
||||
this.createdGroup = Object.assign({}, group, {
|
||||
_links: { self: { href: 'group-selflink' } },
|
||||
_links: {
|
||||
self: {
|
||||
href: 'group-selflink',
|
||||
},
|
||||
object: {
|
||||
href: 'group-objectlink',
|
||||
},
|
||||
},
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||
},
|
||||
@@ -223,17 +234,15 @@ describe('GroupFormComponent', () => {
|
||||
return typeof value === 'object' && value !== null;
|
||||
},
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
router = new RouterMock();
|
||||
route = new ActivatedRouteStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}), GroupFormComponent],
|
||||
TranslateModule.forRoot(),
|
||||
GroupFormComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
@@ -249,14 +258,11 @@ describe('GroupFormComponent', () => {
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(GroupFormComponent, {
|
||||
remove: { imports: [
|
||||
@@ -279,8 +285,8 @@ describe('GroupFormComponent', () => {
|
||||
describe('when submitting the form', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.groupName.value = groupName;
|
||||
component.groupDescription.value = groupDescription;
|
||||
component.groupName.setValue(groupName);
|
||||
component.groupDescription.setValue(groupDescription);
|
||||
});
|
||||
describe('without active Group', () => {
|
||||
beforeEach(() => {
|
||||
@@ -288,14 +294,22 @@ describe('GroupFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new group using the correct values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
it('should emit a new group using the correct values', (() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
name: groupName,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: groupDescription,
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with active Group', () => {
|
||||
let expected2;
|
||||
let expected2: Group;
|
||||
beforeEach(() => {
|
||||
expected2 = Object.assign(new Group(), {
|
||||
name: 'newGroupName',
|
||||
@@ -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, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||
component.groupName.value = 'newGroupName';
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should edit with name and description operations', () => {
|
||||
component.groupName.setValue('newGroupName');
|
||||
component.onSubmit();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
@@ -328,9 +351,8 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
it('should edit with description operations', () => {
|
||||
component.groupName.value = null;
|
||||
component.groupName.setValue(null);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'add',
|
||||
path: '/metadata/dc.description',
|
||||
@@ -340,9 +362,9 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
it('should edit with name operations', () => {
|
||||
component.groupDescription.value = null;
|
||||
component.groupName.setValue('newGroupName');
|
||||
component.groupDescription.setValue(null);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
const operations = [{
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
@@ -351,12 +373,13 @@ describe('GroupFormComponent', () => {
|
||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', (async () => {
|
||||
await fixture.whenStable().then(() => {
|
||||
it('should emit the existing group using the correct new values', () => {
|
||||
component.onSubmit();
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should emit success notification', () => {
|
||||
component.onSubmit();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -371,11 +394,8 @@ describe('GroupFormComponent', () => {
|
||||
|
||||
|
||||
describe('check form validation', () => {
|
||||
let groupCommunity;
|
||||
|
||||
beforeEach(() => {
|
||||
groupName = 'testName';
|
||||
groupCommunity = 'testgroupCommunity';
|
||||
groupDescription = 'testgroupDescription';
|
||||
|
||||
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(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.initialisePage();
|
||||
@@ -438,21 +467,20 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
let deleteButton;
|
||||
let deleteButton: HTMLButtonElement;
|
||||
|
||||
beforeEach(() => {
|
||||
component.initialisePage();
|
||||
|
||||
component.canEdit$ = observableOf(true);
|
||||
component.groupBeingEdited = {
|
||||
beforeEach(async () => {
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
component.activeGroup$ = observableOf({
|
||||
id: 'active-group',
|
||||
permanent: false,
|
||||
} as Group;
|
||||
} as Group);
|
||||
component.canEdit$ = observableOf(true);
|
||||
|
||||
component.initialisePage();
|
||||
|
||||
fixture.detectChanges();
|
||||
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
||||
});
|
||||
|
||||
describe('if confirmed via modal', () => {
|
||||
|
@@ -11,7 +11,10 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
@@ -31,14 +34,13 @@ import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
@@ -53,7 +55,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
@@ -61,9 +62,9 @@ import { Community } from '../../../core/shared/community.model';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import {
|
||||
getAllCompletedRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||
@@ -71,6 +72,7 @@ import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { ContextHelpDirective } from '../../../shared/context-help.directive';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isNotEmpty,
|
||||
@@ -117,9 +119,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Dynamic models for the inputs of form
|
||||
*/
|
||||
groupName: DynamicInputModel;
|
||||
groupCommunity: DynamicInputModel;
|
||||
groupDescription: DynamicTextAreaModel;
|
||||
groupName: AbstractControl;
|
||||
groupCommunity: AbstractControl;
|
||||
groupDescription: AbstractControl;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
@@ -162,21 +164,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Group currently being edited
|
||||
*/
|
||||
groupBeingEdited: Group;
|
||||
|
||||
/**
|
||||
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||
*/
|
||||
canEdit$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
* The current {@link Group}
|
||||
*/
|
||||
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
|
||||
@@ -186,78 +197,71 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
protected formBuilderService: FormBuilderService,
|
||||
protected translateService: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected modalService: NgbModal,
|
||||
public requestService: RequestService,
|
||||
protected changeDetectorRef: ChangeDetectorRef,
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
if (this.route.snapshot.params.groupId !== 'newGroup') {
|
||||
this.setActiveGroup(this.route.snapshot.params.groupId);
|
||||
}
|
||||
this.activeGroup$ = this.groupDataService.getActiveGroup();
|
||||
this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO();
|
||||
this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute();
|
||||
this.canEdit$ = this.activeGroupLinkedDSO$.pipe(
|
||||
filter((dso: DSpaceObject) => hasNoValue(dso)),
|
||||
switchMap(() => this.activeGroup$),
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)),
|
||||
startWith(false),
|
||||
);
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
initialisePage() {
|
||||
this.subs.push(this.route.params.subscribe((params) => {
|
||||
if (params.groupId !== 'newGroup') {
|
||||
this.setActiveGroup(params.groupId);
|
||||
}
|
||||
}));
|
||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
]).pipe(
|
||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
||||
);
|
||||
}),
|
||||
);
|
||||
observableCombineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
||||
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||
this.groupName = new DynamicInputModel({
|
||||
const groupNameModel = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
label: groupName,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
|
||||
name: 'groupName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.groupCommunity = new DynamicInputModel({
|
||||
const groupCommunityModel = new DynamicInputModel({
|
||||
id: 'groupCommunity',
|
||||
label: groupCommunity,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
|
||||
name: 'groupCommunity',
|
||||
required: false,
|
||||
readOnly: true,
|
||||
});
|
||||
this.groupDescription = new DynamicTextAreaModel({
|
||||
const groupDescriptionModel = new DynamicTextAreaModel({
|
||||
id: 'groupDescription',
|
||||
label: groupDescription,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.groupDescription`),
|
||||
name: 'groupDescription',
|
||||
required: false,
|
||||
spellCheck: environment.form.spellCheck,
|
||||
});
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
this.groupDescription,
|
||||
groupNameModel,
|
||||
groupDescriptionModel,
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.groupName = this.formGroup.get('groupName');
|
||||
this.groupDescription = this.formGroup.get('groupDescription');
|
||||
|
||||
if (this.formGroup.controls.groupName) {
|
||||
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||
if (hasValue(this.groupName)) {
|
||||
this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
@@ -265,22 +269,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.subs.push(
|
||||
observableCombineLatest([
|
||||
this.groupDataService.getActiveGroup(),
|
||||
this.activeGroup$,
|
||||
this.canEdit$,
|
||||
this.groupDataService.getActiveGroup()
|
||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))),
|
||||
this.activeGroupLinkedDSO$.pipe(take(1)),
|
||||
]).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.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel);
|
||||
this.groupDescription = this.formGroup.get('groupCommunity');
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupCommunity: linkedObject?.name ?? '',
|
||||
@@ -288,10 +289,6 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
this.groupDescription,
|
||||
];
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||
@@ -305,7 +302,6 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,9 +320,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
||||
(group: Group) => {
|
||||
const values = {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||
if (group === null) {
|
||||
this.createNewGroup({
|
||||
name: this.groupName.value,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
@@ -335,14 +331,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (group === null) {
|
||||
this.createNewGroup(values);
|
||||
});
|
||||
} else {
|
||||
this.editGroup(group);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +441,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* @param groupSelfLink SelfLink of group to set as active
|
||||
*/
|
||||
setActiveGroupWithLink(groupSelfLink: string) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup === null) {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||
@@ -467,7 +460,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
||||
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||
@@ -511,52 +504,37 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
|
||||
* workflow group)
|
||||
*/
|
||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
return this.getLinkedDSO(group).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
return hasValue(rd) && hasValue(rd.payload);
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
*/
|
||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
|
||||
return this.activeGroup$.pipe(
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => {
|
||||
if (group.object === undefined) {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||
}
|
||||
return group.object;
|
||||
}
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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
|
||||
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
|
||||
* to a workflow group) if it has one
|
||||
*/
|
||||
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;
|
||||
getLinkedEditRolesRoute(): Observable<string> {
|
||||
return this.activeGroupLinkedDSO$.pipe(
|
||||
map((dso: DSpaceObject) => {
|
||||
switch ((dso as any).type) {
|
||||
case Community.type.value:
|
||||
return getCommunityEditRolesRoute(rd.payload.id);
|
||||
return getCommunityEditRolesRoute(dso.id);
|
||||
case Collection.type.value:
|
||||
return getCollectionEditRolesRoute(rd.payload.id);
|
||||
}
|
||||
return getCollectionEditRolesRoute(dso.id);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,9 +9,9 @@
|
||||
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
||||
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="pageConfig"
|
||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
<div class="table-responsive">
|
||||
@@ -26,12 +26,12 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
|
||||
<td>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||
@@ -46,13 +46,13 @@
|
||||
</table>
|
||||
</div>
|
||||
</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}}
|
||||
</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" 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" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -8,10 +8,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
cold,
|
||||
hot,
|
||||
} from 'jasmine-marbles';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
@@ -191,17 +188,17 @@ describe('BitstreamFormatsComponent', () => {
|
||||
beforeEach(waitForAsync(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
|
||||
const result = comp.isSelected(bitstreamFormat1);
|
||||
|
||||
expect(result).toBeObservable(cold('b', { b: true }));
|
||||
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
|
||||
});
|
||||
});
|
||||
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
|
||||
const format = new BitstreamFormat();
|
||||
format.uuid = 'new';
|
||||
|
||||
const result = comp.isSelected(format);
|
||||
|
||||
expect(result).toBeObservable(cold('b', { b: false }));
|
||||
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -13,10 +13,7 @@ import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
mergeMap,
|
||||
@@ -58,7 +55,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* 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
|
||||
@@ -125,14 +127,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
||||
* @param bitstreamFormat
|
||||
* Returns the list of all the bitstream formats that are selected in the list (checkbox)
|
||||
*/
|
||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
||||
selectedBitstreamFormatIDs(): Observable<string[]> {
|
||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
||||
}),
|
||||
map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
||||
this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }),
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
|
||||
const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
|
||||
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||
this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||
switchMap((findListOptions: FindListOptions) => {
|
||||
return this.bitstreamFormatService.findAll(findListOptions);
|
||||
}),
|
||||
);
|
||||
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
|
||||
}
|
||||
|
||||
|
||||
|
@@ -27,14 +27,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
|
||||
<td>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(schema) | async"
|
||||
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
|
||||
(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>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||
|
@@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
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 { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
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 { NotificationsServiceStub } from '../../../shared/testing/notifications-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 { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { MetadataRegistryComponent } from './metadata-registry.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', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||
let registryService: RegistryService;
|
||||
let paginationService;
|
||||
const mockSchemasList = [
|
||||
|
||||
let paginationService: PaginationServiceStub;
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
const mockSchemasList: MetadataSchema[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
prefix: 'mock',
|
||||
namespace: 'http://dspace.org/mockschema',
|
||||
},
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
getSelectedMetadataSchemas: () => observableOf([]),
|
||||
editMetadataSchema: (schema) => {
|
||||
},
|
||||
cancelEditMetadataSchema: () => {
|
||||
},
|
||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataSchema: () => {
|
||||
},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
] as MetadataSchema[];
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
@@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => {
|
||||
);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
paginationService = new PaginationServiceStub();
|
||||
registryService = new RegistryServiceStub();
|
||||
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
EnumKeysPipe,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{
|
||||
@@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema));
|
||||
comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema);
|
||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
@@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
|
||||
comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||
comp.deleteSchemas();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -7,19 +7,17 @@ import {
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Router,
|
||||
RouterLink,
|
||||
} from '@angular/router';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
zip,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -36,7 +34,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -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);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected registryService: RegistryService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected paginationService: PaginationService,
|
||||
protected translateService: TranslateService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||
this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe(
|
||||
map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)),
|
||||
);
|
||||
this.updateSchemas();
|
||||
}
|
||||
|
||||
@@ -116,30 +133,13 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
* @param schema
|
||||
*/
|
||||
editSchema(schema: MetadataSchema) {
|
||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
||||
this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
|
||||
if (schema === activeSchema) {
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
} else {
|
||||
this.registryService.editMetadataSchema(schema);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata schema is active (being edited)
|
||||
* @param schema
|
||||
*/
|
||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
||||
return this.getActiveSchema().pipe(
|
||||
map((activeSchema) => schema === activeSchema),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata schema (being edited)
|
||||
*/
|
||||
getActiveSchema(): Observable<MetadataSchema> {
|
||||
return this.registryService.getActiveMetadataSchema();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,31 +153,16 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
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
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
for (const schema of schemas) {
|
||||
if (hasValue(schema.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData()));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
|
||||
take(1),
|
||||
switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
|
||||
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
@@ -186,9 +171,7 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
}
|
||||
this.registryService.deselectAllMetadataSchema();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,20 +182,20 @@ export class MetadataRegistryComponent implements OnDestroy {
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }),
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
|
||||
const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||
const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount });
|
||||
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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>
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../../shared/form/form.component';
|
||||
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 { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||
|
||||
describe('MetadataSchemaFormComponent', () => {
|
||||
let component: MetadataSchemaFormComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
cancelEditMetadataSchema: () => {
|
||||
},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {
|
||||
},
|
||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
registryService = new RegistryServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataSchemaFormComponent, {
|
||||
remove: {
|
||||
@@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
|
||||
describe('without an active schema', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
||||
component.activeMetadataSchema$ = observableOf(undefined);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
} as MetadataSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
||||
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -21,13 +21,13 @@ import {
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
@@ -102,17 +102,24 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@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() {
|
||||
combineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.name`),
|
||||
this.translateService.get(`${this.messagePrefix}.namespace`),
|
||||
]).subscribe(([name, namespace]) => {
|
||||
this.name = new DynamicInputModel({
|
||||
id: 'name',
|
||||
label: name,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.name`),
|
||||
name: 'name',
|
||||
validators: {
|
||||
required: null,
|
||||
@@ -127,7 +134,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.namespace = new DynamicInputModel({
|
||||
id: 'namespace',
|
||||
label: namespace,
|
||||
label: this.translateService.instant(`${this.messagePrefix}.namespace`),
|
||||
name: 'namespace',
|
||||
validators: {
|
||||
required: null,
|
||||
@@ -146,7 +153,8 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
||||
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||
this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => {
|
||||
if (schema == null) {
|
||||
this.clearFields();
|
||||
} else {
|
||||
@@ -158,8 +166,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.name.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,44 +183,25 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit(): void {
|
||||
this.registryService
|
||||
.getActiveMetadataSchema()
|
||||
.pipe(
|
||||
this.activeMetadataSchema$.pipe(
|
||||
take(1),
|
||||
switchMap((schema: MetadataSchema) => {
|
||||
const metadataValues = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value,
|
||||
};
|
||||
|
||||
let createOrUpdate$: Observable<MetadataSchema>;
|
||||
|
||||
if (schema == null) {
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
Object.assign(new MetadataSchema(), metadataValues),
|
||||
);
|
||||
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
|
||||
} else {
|
||||
const updatedSchema = Object.assign(
|
||||
new MetadataSchema(),
|
||||
schema,
|
||||
{
|
||||
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
namespace: metadataValues.namespace,
|
||||
},
|
||||
);
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
updatedSchema,
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
return createOrUpdate$;
|
||||
}),
|
||||
tap(() => {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
}),
|
||||
)
|
||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||
switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
|
||||
map(() => updatedOrCreatedSchema),
|
||||
)),
|
||||
).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedOrCreatedSchema);
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
@@ -233,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { FormComponent } from '../../../../shared/form/form.component';
|
||||
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 { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||
|
||||
describe('MetadataFieldFormComponent', () => {
|
||||
let component: MetadataFieldFormComponent;
|
||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||
let registryService: RegistryService;
|
||||
|
||||
let registryService: RegistryServiceStub;
|
||||
|
||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
@@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => {
|
||||
prefix: 'fake',
|
||||
});
|
||||
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataField: () => {
|
||||
},
|
||||
cancelEditMetadataSchema: () => {
|
||||
},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
return {
|
||||
patchValue: () => {
|
||||
},
|
||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
registryService = new RegistryServiceStub();
|
||||
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataFieldFormComponent, {
|
||||
remove: { imports: [FormComponent] },
|
||||
|
@@ -31,8 +31,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of fields?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td *ngVar="(isSelected(field) | async) as selected">
|
||||
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
|
||||
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||
[checked]="selected"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ComponentFixture,
|
||||
inject,
|
||||
@@ -7,16 +7,12 @@ import {
|
||||
waitForAsync,
|
||||
} from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
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 { NotificationsServiceStub } from '../../../shared/testing/notifications-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 { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
@@ -45,8 +41,12 @@ import { MetadataSchemaComponent } from './metadata-schema.component';
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
||||
let registryService: RegistryService;
|
||||
const mockSchemasList = [
|
||||
|
||||
let registryService: RegistryServiceStub;
|
||||
let activatedRoute: ActivatedRouteStub;
|
||||
let paginationService: PaginationServiceStub;
|
||||
|
||||
const mockSchemasList: MetadataSchema[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => {
|
||||
prefix: 'mock',
|
||||
namespace: 'http://dspace.org/mockschema',
|
||||
},
|
||||
];
|
||||
const mockFieldsList = [
|
||||
] as MetadataSchema[];
|
||||
const mockFieldsList: MetadataField[] = [
|
||||
{
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => {
|
||||
scopeNote: null,
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||
},
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {
|
||||
},
|
||||
cancelEditMetadataField: () => {
|
||||
},
|
||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
||||
deselectAllMetadataField: () => {
|
||||
},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
||||
};
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
] as MetadataField[];
|
||||
const schemaNameParam = 'mock';
|
||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||
params: observableOf({
|
||||
schemaName: schemaNameParam,
|
||||
}),
|
||||
});
|
||||
|
||||
const paginationService = new PaginationServiceStub();
|
||||
|
||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||
@@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => {
|
||||
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
activatedRoute = new ActivatedRouteStub({
|
||||
schemaName: schemaNameParam,
|
||||
});
|
||||
paginationService = new PaginationServiceStub();
|
||||
registryService = new RegistryServiceStub();
|
||||
spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))));
|
||||
spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => {
|
||||
VarDirective,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: RegistryService, useValue: registryService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{
|
||||
provide: NotificationsService,
|
||||
@@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(MetadataSchemaComponent, {
|
||||
remove: {
|
||||
@@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField));
|
||||
comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField);
|
||||
spyOn(registryService, 'cancelEditMetadataField');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
@@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
|
||||
comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id));
|
||||
comp.deleteFields();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -20,9 +20,9 @@ import {
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
zip,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../../../core/shared/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
@@ -96,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
/**
|
||||
* The current {@link MetadataField} that is being edited
|
||||
*/
|
||||
activeField$: Observable<MetadataField>;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
this.route.params.subscribe((params) => {
|
||||
this.initialize(params);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component using the params within the url (schemaName)
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.activeField$ = this.registryService.getActiveMetadataField();
|
||||
this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
|
||||
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
|
||||
);
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
@@ -148,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
* @param field
|
||||
*/
|
||||
editField(field: MetadataField) {
|
||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
||||
this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
|
||||
if (field === activeField) {
|
||||
this.registryService.cancelEditMetadataField();
|
||||
} else {
|
||||
this.registryService.editMetadataField(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given metadata field is active (being edited)
|
||||
* @param field
|
||||
*/
|
||||
isActive(field: MetadataField): Observable<boolean> {
|
||||
return this.getActiveField().pipe(
|
||||
map((activeField) => field === activeField),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active metadata field (being edited)
|
||||
*/
|
||||
getActiveField(): Observable<MetadataField> {
|
||||
return this.registryService.getActiveMetadataField();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,29 +174,14 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
this.registryService.deselectMetadataField(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
||||
* @param field
|
||||
*/
|
||||
isSelected(field: MetadataField): Observable<boolean> {
|
||||
return this.registryService.getSelectedMetadataFields().pipe(
|
||||
map((fields) => fields.find((selectedField) => selectedField === field) != null),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the selected metadata fields
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
for (const field of fields) {
|
||||
if (hasValue(field.id)) {
|
||||
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData()));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
|
||||
take(1),
|
||||
switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
|
||||
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||
if (successResponses.length > 0) {
|
||||
@@ -218,9 +192,7 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
});
|
||||
},
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest([
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }),
|
||||
]);
|
||||
messages.subscribe(([head, content]) => {
|
||||
const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||
const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.registryService.deselectAllMetadataField();
|
||||
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="row">
|
||||
<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">{{ 'pagination.showing.detail' | translate:(getShowingDetails(collectionSize)|async)}}</span>
|
||||
<span class="align-middle">{{ 'pagination.showing.detail' | translate:(showingDetails$ | async)}}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">
|
||||
|
@@ -10,9 +10,11 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
@@ -28,6 +30,8 @@ import {
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
@@ -40,13 +44,21 @@ import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface';
|
||||
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 { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
import { RSSComponent } from '../rss-feed/rss.component';
|
||||
import { EnumKeysPipe } from '../utils/enum-keys-pipe';
|
||||
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||
|
||||
interface PaginationDetails {
|
||||
range: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default pagination controls component.
|
||||
*/
|
||||
@@ -60,7 +72,7 @@ import { PaginationComponentOptions } from './pagination-component-options.model
|
||||
standalone: true,
|
||||
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}.
|
||||
*/
|
||||
@@ -205,6 +217,9 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
public sortField$: Observable<string>;
|
||||
public defaultSortField = 'name';
|
||||
|
||||
|
||||
public showingDetails$: Observable<PaginationDetails>;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
@@ -233,6 +248,12 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
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.
|
||||
*/
|
||||
@@ -270,9 +291,11 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
constructor(private cdRef: ChangeDetectorRef,
|
||||
private paginationService: PaginationService,
|
||||
public hostWindowService: HostWindowService) {
|
||||
constructor(
|
||||
protected cdRef: ChangeDetectorRef,
|
||||
protected paginationService: PaginationService,
|
||||
public hostWindowService: HostWindowService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,10 +349,10 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
/**
|
||||
* Method to get pagination details of the current viewed page.
|
||||
*/
|
||||
public getShowingDetails(collectionSize: number): Observable<any> {
|
||||
let showingDetails = observableOf({ range: null + ' - ' + null, total: null });
|
||||
if (collectionSize) {
|
||||
showingDetails = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe(
|
||||
public getShowingDetails(collectionSize: number): Observable<PaginationDetails> {
|
||||
return observableOf(collectionSize).pipe(
|
||||
hasValueOperator(),
|
||||
switchMap(() => this.paginationService.getCurrentPagination(this.id, this.paginationOptions)),
|
||||
map((currentPaginationOptions) => {
|
||||
let lastItem: number;
|
||||
const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage;
|
||||
@@ -340,12 +363,17 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
} else {
|
||||
lastItem = collectionSize;
|
||||
}
|
||||
return { range: firstItem + ' - ' + lastItem, total: collectionSize };
|
||||
return {
|
||||
range: `${firstItem} - ${lastItem}`,
|
||||
total: collectionSize,
|
||||
};
|
||||
}),
|
||||
startWith({
|
||||
range: `${null} - ${null}`,
|
||||
total: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return showingDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to ensure options passed contains the required properties.
|
||||
|
111
src/app/shared/testing/registry.service.stub.ts
Normal file
111
src/app/shared/testing/registry.service.stub.ts
Normal 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());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user