mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-14 13:33:03 +00:00
Merge branch 'main' into w2p-85192_pr-bugfix-specify-view-mode-in-ds-browse-by
This commit is contained in:
@@ -52,15 +52,17 @@
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -28,6 +28,9 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||
import { FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
@@ -99,12 +102,78 @@ describe('EPersonFormComponent', () => {
|
||||
}
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(ePerson);
|
||||
},
|
||||
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
||||
return createSuccessfulRemoteDataObject$(null);
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
builderService = Object.assign(getMockFormBuilderService(),{
|
||||
createFormGroup(formModel, options = null) {
|
||||
const controls = {};
|
||||
formModel.forEach( model => {
|
||||
model.parent = parent;
|
||||
const controlModel = model;
|
||||
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
|
||||
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
|
||||
controls[model.id] = new FormControl(controlState, controlOptions);
|
||||
});
|
||||
return new FormGroup(controls, options);
|
||||
},
|
||||
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||
return {
|
||||
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
|
||||
};
|
||||
},
|
||||
getValidators(validatorsConfig) {
|
||||
return this.getValidatorFns(validatorsConfig);
|
||||
},
|
||||
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
|
||||
let validatorFns = [];
|
||||
if (this.isObject(validatorsConfig)) {
|
||||
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
|
||||
const validatorConfigValue = validatorsConfig[validatorConfigKey];
|
||||
if (this.isValidatorDescriptor(validatorConfigValue)) {
|
||||
const descriptor = validatorConfigValue;
|
||||
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
|
||||
}
|
||||
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
|
||||
});
|
||||
}
|
||||
return validatorFns;
|
||||
},
|
||||
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
|
||||
let validatorFn;
|
||||
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
|
||||
validatorFn = Validators[validatorName];
|
||||
} else { // Custom Validators
|
||||
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
|
||||
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
|
||||
} else if (validatorsToken) {
|
||||
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
|
||||
}
|
||||
}
|
||||
if (validatorFn === undefined) { // throw when no validator could be resolved
|
||||
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
|
||||
}
|
||||
if (validatorArgs !== null) {
|
||||
return validatorFn(validatorArgs);
|
||||
}
|
||||
return validatorFn;
|
||||
},
|
||||
isValidatorDescriptor(value) {
|
||||
if (this.isObject(value)) {
|
||||
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isObject(value) {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
});
|
||||
authService = new AuthServiceStub();
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
isAuthorized: observableOf(true),
|
||||
|
||||
});
|
||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
@@ -146,6 +215,131 @@ describe('EPersonFormComponent', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
describe('check form validation', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let requireCertificate;
|
||||
|
||||
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;
|
||||
|
||||
fixture.detectChanges();
|
||||
component.initialisePage();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('firstName, lastName and email should be required', () => {
|
||||
it('form should be invalid because the firstName is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
it('form should be invalid because the lastName is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
it('form should be invalid because the email is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('after inserting information firstName,lastName and email not required', () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.firstName.setValue('test');
|
||||
component.formGroup.controls.lastName.setValue('test');
|
||||
component.formGroup.controls.email.setValue('test@test.com');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('firstName should be valid because the firstName is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
it('lastName should be valid because the lastName is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
it('email should be valid because the email is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.email.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.email.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('after inserting email wrong should show pattern validation error', () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.email.setValue('test@test');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('email should not be valid because the email pattern', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('after already utilized email', () => {
|
||||
beforeEach(() => {
|
||||
const ePersonServiceWithEperson = Object.assign(ePersonDataServiceStub,{
|
||||
getEPersonByEmail(): Observable<RemoteData<EPerson>> {
|
||||
return createSuccessfulRemoteDataObject$(EPersonMock);
|
||||
}
|
||||
});
|
||||
component.formGroup.controls.email.setValue('test@test.com');
|
||||
component.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(ePersonServiceWithEperson));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('email should not be valid because email is already taken', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
describe('when submitting the form', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicCheckboxModel,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
@@ -32,10 +32,12 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
templateUrl: './eperson-form.component.html'
|
||||
templateUrl: './eperson-form.component.html',
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing EPeople
|
||||
@@ -160,7 +162,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
isImpersonated = false;
|
||||
|
||||
constructor(public epersonService: EPersonDataService,
|
||||
/**
|
||||
* Subscription to email field value change
|
||||
*/
|
||||
emailValueChangeSubscribe: Subscription;
|
||||
|
||||
constructor(protected changeDetectorRef: ChangeDetectorRef,
|
||||
public epersonService: EPersonDataService,
|
||||
public groupsDataService: GroupDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
@@ -186,6 +194,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
|
||||
observableCombineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
@@ -218,9 +227,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
name: 'email',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
|
||||
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
emailTaken: 'error.validation.emailTaken',
|
||||
pattern: 'error.validation.NotValidEmail'
|
||||
},
|
||||
hint: emailHint
|
||||
});
|
||||
this.canLogIn = new DynamicCheckboxModel(
|
||||
@@ -259,11 +272,18 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
canLogIn: eperson != null ? eperson.canLogIn : true,
|
||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||
});
|
||||
|
||||
if (eperson === null && !!this.formGroup.controls.email) {
|
||||
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
||||
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||
|
||||
this.groups = activeEPerson$.pipe(
|
||||
this.groups = activeEPerson$.pipe(
|
||||
switchMap((eperson) => {
|
||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||
currentPage: 1,
|
||||
@@ -272,14 +292,20 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
switchMap(([eperson, findListOptions]) => {
|
||||
if (eperson != null) {
|
||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions);
|
||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||
}
|
||||
return observableOf(undefined);
|
||||
})
|
||||
);
|
||||
|
||||
this.canImpersonate$ = activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
||||
switchMap((eperson) => {
|
||||
if (hasValue(eperson)) {
|
||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.canDelete$ = activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||
@@ -342,10 +368,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', {name: ePersonToCreate.name}));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||
this.submitForm.emit(ePersonToCreate);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', {name: ePersonToCreate.name}));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -381,10 +407,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
const response = this.epersonService.updateEPerson(editedEperson);
|
||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', {name: editedEperson.name}));
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||
this.submitForm.emit(editedEperson);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', {name: editedEperson.name}));
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
@@ -394,6 +420,87 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.updateGroups({
|
||||
currentPage: event,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start impersonating the EPerson
|
||||
*/
|
||||
impersonate() {
|
||||
this.authService.impersonate(this.epersonInitial.id);
|
||||
this.isImpersonated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = eperson;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||
modalRef.componentInstance.brandColor = 'danger';
|
||||
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
if (hasValue(eperson.id)) {
|
||||
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||
if (restResponse.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
|
||||
this.submitForm.emit();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
}
|
||||
this.cancelForm.emit();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the EPerson
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.authService.stopImpersonatingAndRefresh();
|
||||
this.isImpersonated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
if (hasValue(this.emailValueChangeSubscribe)) {
|
||||
this.emailValueChangeSubscribe.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
this.requestService.removeByHrefSubstring(eperson.self);
|
||||
});
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||
* and shows notification if this is the case
|
||||
@@ -416,17 +523,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.updateGroups({
|
||||
currentPage: event,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of groups by fetching it from the rest api or cache
|
||||
*/
|
||||
@@ -435,71 +531,4 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start impersonating the EPerson
|
||||
*/
|
||||
impersonate() {
|
||||
this.authService.impersonate(this.epersonInitial.id);
|
||||
this.isImpersonated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = eperson;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||
modalRef.componentInstance.brandColor = 'danger';
|
||||
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
if (hasValue(eperson.id)) {
|
||||
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||
if (restResponse.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
|
||||
this.submitForm.emit();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
}
|
||||
this.cancelForm.emit();
|
||||
});
|
||||
}}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the EPerson
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.authService.stopImpersonatingAndRefresh();
|
||||
this.isImpersonated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
this.requestService.removeByHrefSubstring(eperson.self);
|
||||
});
|
||||
this.initialisePage();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { getFirstSucceededRemoteData, } from '../../../../core/shared/operators';
|
||||
|
||||
export class ValidateEmailNotTaken {
|
||||
|
||||
/**
|
||||
* This method will create the validator with the ePersonDataService requested from component
|
||||
* @param ePersonDataService the service with DI in the component that this validator is being utilized.
|
||||
*/
|
||||
static createValidator(ePersonDataService: EPersonDataService) {
|
||||
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
|
||||
return ePersonDataService.getEPersonByEmail(control.value)
|
||||
.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map(res => {
|
||||
return !!res.payload ? { emailTaken: true } : null;
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule, FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -34,6 +34,7 @@ import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mo
|
||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
@@ -117,7 +118,69 @@ describe('GroupFormComponent', () => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
builderService = Object.assign(getMockFormBuilderService(),{
|
||||
createFormGroup(formModel, options = null) {
|
||||
const controls = {};
|
||||
formModel.forEach( model => {
|
||||
model.parent = parent;
|
||||
const controlModel = model;
|
||||
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
|
||||
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
|
||||
controls[model.id] = new FormControl(controlState, controlOptions);
|
||||
});
|
||||
return new FormGroup(controls, options);
|
||||
},
|
||||
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||
return {
|
||||
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
|
||||
};
|
||||
},
|
||||
getValidators(validatorsConfig) {
|
||||
return this.getValidatorFns(validatorsConfig);
|
||||
},
|
||||
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
|
||||
let validatorFns = [];
|
||||
if (this.isObject(validatorsConfig)) {
|
||||
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
|
||||
const validatorConfigValue = validatorsConfig[validatorConfigKey];
|
||||
if (this.isValidatorDescriptor(validatorConfigValue)) {
|
||||
const descriptor = validatorConfigValue;
|
||||
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
|
||||
}
|
||||
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
|
||||
});
|
||||
}
|
||||
return validatorFns;
|
||||
},
|
||||
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
|
||||
let validatorFn;
|
||||
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
|
||||
validatorFn = Validators[validatorName];
|
||||
} else { // Custom Validators
|
||||
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
|
||||
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
|
||||
} else if (validatorsToken) {
|
||||
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
|
||||
}
|
||||
}
|
||||
if (validatorFn === undefined) { // throw when no validator could be resolved
|
||||
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
|
||||
}
|
||||
if (validatorArgs !== null) {
|
||||
return validatorFn(validatorArgs);
|
||||
}
|
||||
return validatorFn;
|
||||
},
|
||||
isValidatorDescriptor(value) {
|
||||
if (this.isObject(value)) {
|
||||
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isObject(value) {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
router = new RouterMock();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
@@ -217,4 +280,72 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('check form validation', () => {
|
||||
let groupCommunity;
|
||||
|
||||
beforeEach(() => {
|
||||
groupName = 'testName';
|
||||
groupCommunity = 'testgroupCommunity';
|
||||
groupDescription = 'testgroupDescription';
|
||||
|
||||
expected = Object.assign(new Group(), {
|
||||
name: groupName,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: groupDescription
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
spyOn(component.submitForm, 'emit');
|
||||
|
||||
fixture.detectChanges();
|
||||
component.initialisePage();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('groupName, groupCommunity and groupDescription should be required', () => {
|
||||
it('form should be invalid because the groupName is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.groupName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.groupName.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('after inserting information groupName,groupCommunity and groupDescription not required', () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.groupName.setValue('test');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('groupName should be valid because the groupName is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.groupName.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.groupName.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('after already utilized groupName', () => {
|
||||
beforeEach(() => {
|
||||
const groupsDataServiceStubWithGroup = Object.assign(groupsDataServiceStub,{
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [expected]));
|
||||
}
|
||||
});
|
||||
component.formGroup.controls.groupName.setValue('testName');
|
||||
component.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(groupsDataServiceStubWithGroup));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('groupName should not be valid because groupName is already taken', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.groupName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.groupName.errors.groupExists).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||
import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators';
|
||||
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||
@@ -34,7 +34,8 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
@@ -44,6 +45,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-group-form',
|
||||
@@ -65,6 +67,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
* Dynamic models for the inputs of form
|
||||
*/
|
||||
groupName: DynamicInputModel;
|
||||
groupCommunity: DynamicInputModel;
|
||||
groupDescription: DynamicTextAreaModel;
|
||||
|
||||
/**
|
||||
@@ -124,17 +127,24 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
/**
|
||||
* Subscription to email field value change
|
||||
*/
|
||||
groupNameValueChangeSubscribe: Subscription;
|
||||
|
||||
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService) {
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService,
|
||||
protected changeDetectorRef: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -160,8 +170,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
observableCombineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||
).subscribe(([groupName, groupDescription]) => {
|
||||
).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||
this.groupName = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
label: groupName,
|
||||
@@ -171,6 +182,13 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.groupCommunity = new DynamicInputModel({
|
||||
id: 'groupCommunity',
|
||||
label: groupCommunity,
|
||||
name: 'groupCommunity',
|
||||
required: false,
|
||||
readOnly: true,
|
||||
});
|
||||
this.groupDescription = new DynamicTextAreaModel({
|
||||
id: 'groupDescription',
|
||||
label: groupDescription,
|
||||
@@ -182,20 +200,51 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.groupDescription,
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
|
||||
if (!!this.formGroup.controls.groupName) {
|
||||
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.groupDataService.getActiveGroup(),
|
||||
this.canEdit$
|
||||
).subscribe(([activeGroup, canEdit]) => {
|
||||
this.canEdit$,
|
||||
this.groupDataService.getActiveGroup()
|
||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
||||
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||
|
||||
if (activeGroup != null) {
|
||||
|
||||
// Disable group name exists validator
|
||||
this.formGroup.controls.groupName.clearAsyncValidators();
|
||||
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup != null ? activeGroup.name : '',
|
||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
||||
});
|
||||
if (!canEdit || activeGroup.permanent) {
|
||||
this.formGroup.disable();
|
||||
|
||||
if (linkedObject?.name) {
|
||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupCommunity: linkedObject?.name ?? '',
|
||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||
});
|
||||
} else {
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
this.groupDescription,
|
||||
];
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!canEdit || activeGroup.permanent) {
|
||||
this.formGroup.disable();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -407,6 +456,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
|
||||
if ( hasValue(this.groupNameValueChangeSubscribe) ) {
|
||||
this.groupNameValueChangeSubscribe.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,11 +471,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
return this.getLinkedDSO(group).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return hasValue(rd) && hasValue(rd.payload);
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
);
|
||||
|
@@ -38,17 +38,22 @@
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
||||
<td>{{ePerson.eperson.id}}</td>
|
||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||
<td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(ePerson.memberOfGroup)"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
@@ -91,17 +96,22 @@
|
||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
||||
<td>{{ePerson.eperson.id}}</td>
|
||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||
<td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
|
@@ -35,17 +35,19 @@
|
||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="deleteSubgroupFromGroup(group)"
|
||||
@@ -88,17 +90,19 @@
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td>
|
||||
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
|
@@ -17,6 +17,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { NoContent } from '../../../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* Keys to keep track of specific subscriptions
|
||||
@@ -117,7 +118,10 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
}
|
||||
},
|
||||
true,
|
||||
true,
|
||||
followLink('object')
|
||||
))
|
||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.subGroups$.next(rd);
|
||||
@@ -217,7 +221,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
}))
|
||||
}, true, true, followLink('object')
|
||||
))
|
||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.searchResults$.next(rd);
|
||||
}));
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map} from 'rxjs/operators';
|
||||
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
|
||||
export class ValidateGroupExists {
|
||||
|
||||
/**
|
||||
* This method will create the validator with the groupDataService requested from component
|
||||
* @param groupDataService the service with DI in the component that this validator is being utilized.
|
||||
* @return Observable<ValidationErrors | null>
|
||||
*/
|
||||
static createValidator(groupDataService: GroupDataService) {
|
||||
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
|
||||
return groupDataService.searchGroups(control.value, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 100
|
||||
})
|
||||
.pipe(
|
||||
getFirstSucceededRemoteListPayload(),
|
||||
map( (groups: Group[]) => {
|
||||
return groups.filter(group => group.name === control.value);
|
||||
}),
|
||||
map( (groups: Group[]) => {
|
||||
return groups.length > 0 ? { groupExists: true } : null;
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
@@ -48,6 +48,7 @@
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
@@ -56,6 +57,7 @@
|
||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||
<td>{{groupDto.group.id}}</td>
|
||||
<td>{{groupDto.group.name}}</td>
|
||||
<td>{{(groupDto.group.object | async)?.payload?.name}}</td>
|
||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
|
@@ -152,6 +152,7 @@ describe('GroupRegistryComponent', () => {
|
||||
return createSuccessfulRemoteDataObject$(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
|
||||
setIsAuthorized(true, true);
|
||||
paginationService = new PaginationServiceStub();
|
||||
@@ -200,6 +201,13 @@ describe('GroupRegistryComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display community/collection name if present', () => {
|
||||
const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)'));
|
||||
expect(collectionNamesFound.length).toEqual(2);
|
||||
expect(collectionNamesFound[0].nativeElement.textContent).toEqual('');
|
||||
expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName');
|
||||
});
|
||||
|
||||
describe('edit buttons', () => {
|
||||
describe('when the user is a general admin', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
@@ -213,7 +221,7 @@ describe('GroupRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
it('should be active', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
@@ -247,7 +255,7 @@ describe('GroupRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
it('should be active', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||
@@ -266,7 +274,7 @@ describe('GroupRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
it('should not be active', () => {
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||
expect(editButtonsFound.length).toEqual(2);
|
||||
editButtonsFound.forEach((editButtonFound) => {
|
||||
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
||||
|
@@ -35,6 +35,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-groups-registry',
|
||||
@@ -132,8 +133,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
currentPage: paginationOptions.currentPage,
|
||||
elementsPerPage: paginationOptions.pageSize
|
||||
});
|
||||
elementsPerPage: paginationOptions.pageSize,
|
||||
}, true, true, followLink('object'));
|
||||
}),
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
|
@@ -1,10 +1,23 @@
|
||||
<li class="sidebar-section">
|
||||
<a href="javascript:void(0);" class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
||||
<div class="sidebar-section">
|
||||
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
||||
[ngClass]="{ disabled: !hasLink }"
|
||||
[attr.aria-disabled]="!hasLink"
|
||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||
[title]="('menu.section.icon.' + section.id) | translate"
|
||||
[routerLink]="itemModel.link"
|
||||
(keyup.space)="navigate($event)"
|
||||
(keyup.enter)="navigate($event)"
|
||||
href="javascript:void(0);"
|
||||
>
|
||||
<div class="shortcut-icon">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<div class="toggle">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
{{itemModel.text | translate}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<a class="nav-item nav-link" tabindex="-1" [routerLink]="itemModel.link">{{itemModel.text | translate}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
@@ -5,12 +5,15 @@ import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||
import { MenuSection } from '../../../shared/menu/menu.reducer';
|
||||
import { isNotEmpty } from '../../../shared/empty.util';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Represents a non-expandable section in the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-admin-sidebar-section',
|
||||
/* tslint:disable:component-selector */
|
||||
selector: 'li[ds-admin-sidebar-section]',
|
||||
templateUrl: './admin-sidebar-section.component.html',
|
||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||
|
||||
@@ -23,12 +26,26 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
||||
*/
|
||||
menuID: MenuID = MenuID.ADMIN;
|
||||
itemModel;
|
||||
constructor(@Inject('sectionDataProvider') menuSection: MenuSection, protected menuService: MenuService, protected injector: Injector,) {
|
||||
hasLink: boolean;
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
this.itemModel = menuSection.model as LinkMenuItemModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hasLink = isNotEmpty(this.itemModel?.link);
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
navigate(event: any): void {
|
||||
event.preventDefault();
|
||||
if (this.hasLink) {
|
||||
this.router.navigate(this.itemModel.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,24 +4,26 @@
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
params: {sidebarWidth: (sidebarWidth | async)}
|
||||
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||
*ngIf="menuVisible | async" (mouseenter)="expandPreview($event)"
|
||||
(mouseleave)="collapsePreview($event)"
|
||||
*ngIf="menuVisible | async"
|
||||
(mouseenter)="handleMouseEnter($event)"
|
||||
(mouseleave)="handleMouseLeave($event)"
|
||||
role="navigation" [attr.aria-label]="'menu.header.admin.description' |translate">
|
||||
<div class="sidebar-top-level-items">
|
||||
<ul class="navbar-nav">
|
||||
<li class="admin-menu-header sidebar-section">
|
||||
<a class="shortcut-icon navbar-brand mr-0" href="javascript:void(0);">
|
||||
<span class="logo-wrapper">
|
||||
<li class="admin-menu-header">
|
||||
<div class="sidebar-section">
|
||||
<div href="javascript:void(0);" class="nav-item d-flex flex-row flex-nowrap py-0">
|
||||
<div class="shortcut-icon navbar-brand logo-wrapper">
|
||||
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
||||
[alt]="('menu.header.image.logo') | translate">
|
||||
</span>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="navbar-brand mr-0" href="javascript:void(0);">
|
||||
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
|
||||
translate}}</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-collapsible navbar-brand">
|
||||
<div class="mr-0">
|
||||
<h4 class="section-header-text mb-0">{{ 'menu.header.admin' | translate }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
@@ -32,22 +34,22 @@
|
||||
</div>
|
||||
<div class="navbar-nav">
|
||||
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
||||
<a class="nav-item nav-link shortcut-icon"
|
||||
<a class="nav-item nav-link sidebar-section d-flex flex-row flex-nowrap"
|
||||
href="javascript:void(0);"
|
||||
(click)="toggle($event)">
|
||||
(click)="toggle($event)"
|
||||
(keyup.space)="toggle($event)"
|
||||
>
|
||||
<div class="shortcut-icon">
|
||||
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||
[title]="'menu.section.icon.pin' | translate"></i>
|
||||
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
||||
[title]="'menu.section.icon.unpin' | translate"></i>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link sidebar-section"
|
||||
href="javascript:void(0);"
|
||||
(click)="toggle($event)">
|
||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@@ -25,6 +25,11 @@
|
||||
.navbar-nav {
|
||||
.admin-menu-header {
|
||||
background-color: var(--ds-admin-sidebar-header-bg);
|
||||
|
||||
.sidebar-section {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
img {
|
||||
height: 20px;
|
||||
@@ -34,6 +39,10 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,26 +53,64 @@
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
background-color: var(--ds-admin-sidebar-bg);
|
||||
overflow-x: visible;
|
||||
|
||||
.nav-item {
|
||||
padding-top: var(--bs-spacer);
|
||||
padding-bottom: var(--bs-spacer);
|
||||
background-color: inherit;
|
||||
|
||||
&:focus-visible {
|
||||
// since links fill the whole sidebar, we _inset_ the outline
|
||||
outline-offset: -4px;
|
||||
|
||||
// replace padding with margins so it doesn't extend over the :focus-visible outline
|
||||
// → can't remove the padding altogether; the icon needs to fill out
|
||||
// the collapsed width of the sidebar for the slide animation to look decent.
|
||||
.shortcut-icon {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: var(--ds-icon-padding);
|
||||
margin-right: var(--ds-icon-padding);
|
||||
}
|
||||
.logo-wrapper {
|
||||
margin-right: var(--bs-navbar-padding-x) !important;
|
||||
}
|
||||
.navbar-brand {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: var(--bs-navbar-brand-padding-y);
|
||||
margin-bottom: var(--bs-navbar-brand-padding-y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-icon {
|
||||
background-color: inherit;
|
||||
padding-left: var(--ds-icon-padding);
|
||||
padding-right: var(--ds-icon-padding);
|
||||
}
|
||||
.shortcut-icon, .icon-wrapper {
|
||||
background-color: inherit;
|
||||
z-index: var(--ds-icon-z-index);
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.sidebar-collapsible {
|
||||
padding-left: 0;
|
||||
padding-right: var(--bs-spacer);
|
||||
width: var(--ds-sidebar-items-width);
|
||||
position: relative;
|
||||
a {
|
||||
padding-right: var(--bs-spacer);
|
||||
width: 100%;
|
||||
.toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-top: var(--bs-spacer);
|
||||
|
||||
li a {
|
||||
padding-left: var(--bs-spacer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active > .sidebar-collapsible > .nav-link {
|
||||
color: var(--bs-navbar-dark-active-color);
|
||||
}
|
||||
|
@@ -113,25 +113,10 @@ describe('AdminSidebarComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the collapse icon is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the collapse link is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > a'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { combineLatest, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { first, map, take } from 'rxjs/operators';
|
||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, first, map, take, distinctUntilChanged, withLatestFrom } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
@@ -60,6 +60,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
*/
|
||||
sidebarExpanded: Observable<boolean>;
|
||||
|
||||
inFocus$: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private variableService: CSSVariableService,
|
||||
@@ -69,6 +71,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
private scriptDataService: ScriptDataService,
|
||||
) {
|
||||
super(menuService, injector);
|
||||
this.inFocus$ = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,10 +92,25 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
this.sidebarOpen = !collapsed;
|
||||
this.sidebarClosed = collapsed;
|
||||
});
|
||||
this.sidebarExpanded = observableCombineLatest(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
this.sidebarExpanded = combineLatest([this.menuCollapsed, this.menuPreviewCollapsed])
|
||||
.pipe(
|
||||
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||
);
|
||||
this.inFocus$.pipe(
|
||||
debounceTime(50),
|
||||
distinctUntilChanged(), // disregard focusout in situations like --(focusout)-(focusin)--
|
||||
withLatestFrom(
|
||||
combineLatest([this.menuCollapsed, this.menuPreviewCollapsed])
|
||||
),
|
||||
).subscribe(([inFocus, [collapsed, previewCollapsed]]) => {
|
||||
if (collapsed) {
|
||||
if (inFocus && previewCollapsed) {
|
||||
this.expandPreview(new Event('focusin → expand'));
|
||||
} else if (!inFocus && !previewCollapsed) {
|
||||
this.collapsePreview(new Event('focusout → collapse'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -590,6 +608,32 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('focusin')
|
||||
public handleFocusIn() {
|
||||
this.inFocus$.next(true);
|
||||
}
|
||||
|
||||
@HostListener('focusout')
|
||||
public handleFocusOut() {
|
||||
this.inFocus$.next(false);
|
||||
}
|
||||
|
||||
public handleMouseEnter(event: any) {
|
||||
if (!this.inFocus$.getValue()) {
|
||||
this.expandPreview(event);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public handleMouseLeave(event: any) {
|
||||
if (!this.inFocus$.getValue()) {
|
||||
this.collapsePreview(event);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||
* @param event The animation event
|
||||
|
@@ -1,27 +1,36 @@
|
||||
<li class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
||||
<div class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
||||
[@bgColor]="{
|
||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||
params: {endColor: (sidebarActiveBg | async)}}">
|
||||
<div class="icon-wrapper">
|
||||
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="javascript:void(0);">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</a>
|
||||
<div class="nav-item nav-link d-flex flex-row flex-nowrap"
|
||||
role="button" tabindex="0"
|
||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||
[attr.aria-expanded]="expanded | async"
|
||||
[title]="('menu.section.icon.' + section.id) | translate"
|
||||
(click)="toggleSection($event)"
|
||||
(keyup.space)="toggleSection($event)"
|
||||
(keyup.enter)="toggleSection($event)"
|
||||
>
|
||||
<div class="shortcut-icon h-100">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link" href="javascript:void(0);" tabindex="-1"
|
||||
(click)="toggleSection($event)">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="toggle">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'"
|
||||
[title]="('menu.section.toggle.' + section.id) | translate"
|
||||
></i>
|
||||
</div>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
list-style: disc;
|
||||
color: var(--bs-navbar-dark-color);
|
||||
overflow: hidden;
|
||||
|
||||
margin-bottom: calc(-1 * var(--bs-spacer)); // the bottom-most nav-item is padded, no need for double spacing
|
||||
}
|
||||
|
||||
.sidebar-collapsible {
|
||||
|
@@ -10,6 +10,8 @@ import { Component } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
|
||||
describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
let component: ExpandableAdminSidebarSectionComponent;
|
||||
@@ -24,6 +26,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
{ provide: 'sectionDataProvider', useValue: { icon: iconString } },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
]
|
||||
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
|
||||
set: {
|
||||
@@ -46,29 +49,14 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
});
|
||||
|
||||
it('should set the right icon', () => {
|
||||
const icon = fixture.debugElement.query(By.css('.icon-wrapper')).query(By.css('i.fas'));
|
||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon > i.fas'));
|
||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||
});
|
||||
|
||||
describe('when the icon is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('a.shortcut-icon'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleActiveSection on the menuService', () => {
|
||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the header text is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section > div.nav-item'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
|
@@ -9,12 +9,14 @@ import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Represents a expandable section in the sidebar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-expandable-admin-sidebar-section',
|
||||
/* tslint:disable:component-selector */
|
||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||
animations: [rotate, slide, bgColor]
|
||||
@@ -48,9 +50,14 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
||||
*/
|
||||
expanded: Observable<boolean>;
|
||||
|
||||
constructor(@Inject('sectionDataProvider') menuSection, protected menuService: MenuService,
|
||||
private variableService: CSSVariableService, protected injector: Injector) {
|
||||
super(menuSection, menuService, injector);
|
||||
constructor(
|
||||
@Inject('sectionDataProvider') menuSection,
|
||||
protected menuService: MenuService,
|
||||
private variableService: CSSVariableService,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
) {
|
||||
super(menuSection, menuService, injector, router);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,7 @@ import { Collection } from './core/shared/collection.model';
|
||||
import { Item } from './core/shared/item.model';
|
||||
import { getCommunityPageRoute } from './community-page/community-page-routing-paths';
|
||||
import { getCollectionPageRoute } from './collection-page/collection-page-routing-paths';
|
||||
import { getItemPageRoute } from './item-page/item-page-routing-paths';
|
||||
import { getItemModuleRoute, getItemPageRoute } from './item-page/item-page-routing-paths';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { URLCombiner } from './core/url-combiner/url-combiner';
|
||||
|
||||
@@ -22,6 +22,15 @@ export function getBitstreamModuleRoute() {
|
||||
export function getBitstreamDownloadRoute(bitstream): string {
|
||||
return new URLCombiner(getBitstreamModuleRoute(), bitstream.uuid, 'download').toString();
|
||||
}
|
||||
export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: string, queryParams: any } {
|
||||
const url = new URLCombiner(getItemModuleRoute(), item.uuid, 'request-a-copy').toString();
|
||||
return {
|
||||
routerLink: url,
|
||||
queryParams: {
|
||||
bitstream: bitstream.uuid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const ADMIN_MODULE_PATH = 'admin';
|
||||
|
||||
@@ -90,3 +99,8 @@ export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
export function getAccessControlModuleRoute() {
|
||||
return `/${ACCESS_CONTROL_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
export const REQUEST_COPY_MODULE_PATH = 'request-a-copy';
|
||||
export function getRequestCopyModulePath() {
|
||||
return `/${REQUEST_COPY_MODULE_PATH}`;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
PROFILE_MODULE_PATH,
|
||||
REGISTER_PATH,
|
||||
WORKFLOW_ITEM_MODULE_PATH,
|
||||
LEGACY_BITSTREAM_MODULE_PATH,
|
||||
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
|
||||
} from './app-routing-paths';
|
||||
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||
@@ -180,6 +180,11 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
path: INFO_MODULE_PATH,
|
||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
||||
},
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
|
||||
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||
},
|
||||
{
|
||||
path: FORBIDDEN_PATH,
|
||||
component: ThemedForbiddenComponent
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { delay, distinctUntilChanged, filter, take, withLatestFrom } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, filter, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
Optional,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
NavigationStart, ResolveEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
@@ -71,6 +77,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
*/
|
||||
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
isThemeCSSLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Whether or not the idle modal is is currently open
|
||||
@@ -105,7 +112,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
// the theme css will never download server side, so this should only happen on the browser
|
||||
this.isThemeLoading$.next(true);
|
||||
this.isThemeCSSLoading$.next(true);
|
||||
}
|
||||
if (hasValue(themeName)) {
|
||||
this.setThemeCss(themeName);
|
||||
@@ -177,17 +184,33 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.router.events.pipe(
|
||||
// This fixes an ExpressionChangedAfterItHasBeenCheckedError from being thrown while loading the component
|
||||
// More information on this bug-fix: https://blog.angular-university.io/angular-debugging/
|
||||
delay(0)
|
||||
).subscribe((event) => {
|
||||
let resolveEndFound = false;
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
resolveEndFound = false;
|
||||
this.isRouteLoading$.next(true);
|
||||
this.isThemeLoading$.next(true);
|
||||
} else if (event instanceof ResolveEnd) {
|
||||
resolveEndFound = true;
|
||||
const activatedRouteSnapShot: ActivatedRouteSnapshot = event.state.root;
|
||||
this.themeService.updateThemeOnRouteChange$(event.urlAfterRedirects, activatedRouteSnapShot).pipe(
|
||||
switchMap((changed) => {
|
||||
if (changed) {
|
||||
return this.isThemeCSSLoading$;
|
||||
} else {
|
||||
return [false];
|
||||
}
|
||||
})
|
||||
).subscribe((changed) => {
|
||||
this.isThemeLoading$.next(changed);
|
||||
});
|
||||
} else if (
|
||||
event instanceof NavigationEnd ||
|
||||
event instanceof NavigationCancel
|
||||
) {
|
||||
if (!resolveEndFound) {
|
||||
this.isThemeLoading$.next(false);
|
||||
}
|
||||
this.isRouteLoading$.next(false);
|
||||
}
|
||||
});
|
||||
@@ -237,7 +260,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
// the fact that this callback is used, proves we're on the browser.
|
||||
this.isThemeLoading$.next(false);
|
||||
this.isThemeCSSLoading$.next(false);
|
||||
};
|
||||
head.appendChild(link);
|
||||
}
|
||||
|
@@ -7,7 +7,11 @@ import { EffectsModule } from '@ngrx/effects';
|
||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
DYNAMIC_MATCHER_PROVIDERS,
|
||||
DynamicErrorMessagesMatcher
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||
|
||||
@@ -52,6 +56,7 @@ import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
|
||||
import { UUIDService } from './core/shared/uuid.service';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
|
||||
export function getBase() {
|
||||
return environment.ui.nameSpace;
|
||||
@@ -61,6 +66,14 @@ export function getMetaReducers(): MetaReducer<AppState>[] {
|
||||
return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition for displaying error messages on email form field
|
||||
*/
|
||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||
};
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
@@ -146,6 +159,10 @@ const PROVIDERS = [
|
||||
multi: true,
|
||||
deps: [ CookieService, UUIDService ]
|
||||
},
|
||||
{
|
||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||
useValue: ValidateEmailErrorStateMatcher
|
||||
},
|
||||
...DYNAMIC_MATCHER_PROVIDERS,
|
||||
];
|
||||
|
||||
|
@@ -8,7 +8,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
describe('BreadcrumbsComponent', () => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { BreadcrumbsService } from './breadcrumbs.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component representing the breadcrumbs of a page
|
||||
|
@@ -1,18 +1,27 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormOptionConfig,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
DynamicSelectModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
import { EntityTypeService } from '../../core/data/entity-type.service';
|
||||
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
|
||||
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
|
||||
import { collectionFormEntityTypeSelectionConfig, collectionFormModels, } from './collection-form.models';
|
||||
import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type.resource-type';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing collections
|
||||
@@ -22,7 +31,7 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit {
|
||||
/**
|
||||
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||
*/
|
||||
@@ -34,46 +43,16 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
type = Collection.type;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
* The dynamic form field used for entity type selection
|
||||
* @type {DynamicSelectModel<string>}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'provenance',
|
||||
name: 'dc.description.provenance',
|
||||
}),
|
||||
];
|
||||
entityTypeSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig);
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {DynamicFormControlModel[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
public constructor(protected formService: DynamicFormService,
|
||||
protected translate: TranslateService,
|
||||
@@ -81,7 +60,43 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
protected authService: AuthService,
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected entityTypeService: EntityTypeService) {
|
||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
let currentRelationshipValue: MetadataValue[];
|
||||
if (this.dso && this.dso.metadata) {
|
||||
currentRelationshipValue = this.dso.metadata['dspace.entity.type'];
|
||||
}
|
||||
|
||||
const entities$: Observable<ItemType[]> = this.entityTypeService.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe(
|
||||
getFirstSucceededRemoteListPayload()
|
||||
);
|
||||
|
||||
// retrieve all entity types to populate the dropdowns selection
|
||||
entities$.subscribe((entityTypes: ItemType[]) => {
|
||||
|
||||
entityTypes
|
||||
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
|
||||
.forEach((type: ItemType, index: number) => {
|
||||
this.entityTypeSelection.add({
|
||||
disabled: false,
|
||||
label: type.label,
|
||||
value: type.label
|
||||
} as DynamicFormOptionConfig<string>);
|
||||
if (currentRelationshipValue && currentRelationshipValue.length > 0 && currentRelationshipValue[0].value === type.label) {
|
||||
this.entityTypeSelection.select(index);
|
||||
this.entityTypeSelection.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.formModel = [...collectionFormModels, this.entityTypeSelection];
|
||||
|
||||
super.ngOnInit();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,46 @@
|
||||
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
|
||||
|
||||
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
|
||||
id: 'entityType',
|
||||
name: 'dspace.entity.type',
|
||||
disabled: false
|
||||
};
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
export const collectionFormModels: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
})
|
||||
];
|
@@ -27,7 +27,7 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item
|
||||
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
|
||||
import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service.stub';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { ErrorComponent } from '../../shared/error/error.component';
|
||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<div *ngVar="(contentSource$ |async) as contentSource">
|
||||
<div class="container-fluid" *ngIf="shouldShow">
|
||||
<h4>{{ 'collection.source.controls.head' | translate }}</h4>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.status' | translate}}</span>
|
||||
<span>{{contentSource?.harvestStatus}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.start' | translate}}</span>
|
||||
<span>{{contentSource?.harvestStartTime ? contentSource?.harvestStartTime : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
|
||||
<span>{{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span>
|
||||
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
|
||||
</div>
|
||||
|
||||
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"
|
||||
[disabled]="!(isEnabled)"
|
||||
(click)="testConfiguration(contentSource)">
|
||||
<span>{{'collection.source.controls.test.submit' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(testConfigRunning$ |async)" class="btn btn-secondary"
|
||||
[disabled]="true">
|
||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||
<span>{{'collection.source.controls.test.running' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="!(importRunning$ |async)" class="btn btn-primary"
|
||||
[disabled]="!(isEnabled)"
|
||||
(click)="importNow()">
|
||||
<span class="d-none d-sm-inline">{{'collection.source.controls.import.submit' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(importRunning$ |async)" class="btn btn-primary"
|
||||
[disabled]="true">
|
||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||
<span class="d-none d-sm-inline">{{'collection.source.controls.import.running' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="!(reImportRunning$ |async)" class="btn btn-primary"
|
||||
[disabled]="!(isEnabled)"
|
||||
(click)="resetAndReimport()">
|
||||
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.submit' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(reImportRunning$ |async)" class="btn btn-primary"
|
||||
[disabled]="true">
|
||||
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||
<span class="d-none d-sm-inline"> {{'collection.source.controls.reset.running' | translate}}</span>
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
.spinner-button {
|
||||
margin-bottom: calc((var(--bs-line-height-base) * 1rem - var(--bs-font-size-base)) / 2);
|
||||
}
|
@@ -0,0 +1,232 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { ContentSource } from '../../../../core/shared/content-source.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { CollectionDataService } from '../../../../core/data/collection-data.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
|
||||
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CollectionSourceControlsComponent } from './collection-source-controls.component';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
|
||||
|
||||
describe('CollectionSourceControlsComponent', () => {
|
||||
let comp: CollectionSourceControlsComponent;
|
||||
let fixture: ComponentFixture<CollectionSourceControlsComponent>;
|
||||
|
||||
const uuid = '29481ed7-ae6b-409a-8c51-34dd347a0ce4';
|
||||
let contentSource: ContentSource;
|
||||
let collection: Collection;
|
||||
let process: Process;
|
||||
let bitstream: Bitstream;
|
||||
|
||||
let scriptDataService: ScriptDataService;
|
||||
let processDataService: ProcessDataService;
|
||||
let requestService: RequestService;
|
||||
let notificationsService;
|
||||
let collectionService: CollectionDataService;
|
||||
let httpClient: HttpClient;
|
||||
let bitstreamService: BitstreamDataService;
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
contentSource = Object.assign(new ContentSource(), {
|
||||
uuid: uuid,
|
||||
metadataConfigs: [
|
||||
{
|
||||
id: 'dc',
|
||||
label: 'Simple Dublin Core',
|
||||
nameSpace: 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
||||
},
|
||||
{
|
||||
id: 'qdc',
|
||||
label: 'Qualified Dublin Core',
|
||||
nameSpace: 'http://purl.org/dc/terms/'
|
||||
},
|
||||
{
|
||||
id: 'dim',
|
||||
label: 'DSpace Intermediate Metadata',
|
||||
nameSpace: 'http://www.dspace.org/xmlns/dspace/dim'
|
||||
}
|
||||
],
|
||||
oaiSource: 'oai-harvest-source',
|
||||
oaiSetId: 'oai-set-id',
|
||||
_links: {self: {href: 'contentsource-selflink'}}
|
||||
});
|
||||
process = Object.assign(new Process(), {
|
||||
processId: 'process-id', processStatus: 'COMPLETED',
|
||||
_links: {output: {href: 'output-href'}}
|
||||
});
|
||||
|
||||
bitstream = Object.assign(new Bitstream(), {_links: {content: {href: 'content-href'}}});
|
||||
|
||||
collection = Object.assign(new Collection(), {
|
||||
uuid: 'fake-collection-id',
|
||||
_links: {self: {href: 'collection-selflink'}}
|
||||
});
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
collectionService = jasmine.createSpyObj('collectionService', {
|
||||
getContentSource: createSuccessfulRemoteDataObject$(contentSource),
|
||||
findByHref: createSuccessfulRemoteDataObject$(collection)
|
||||
});
|
||||
scriptDataService = jasmine.createSpyObj('scriptDataService', {
|
||||
invoke: createSuccessfulRemoteDataObject$(process),
|
||||
});
|
||||
processDataService = jasmine.createSpyObj('processDataService', {
|
||||
findById: createSuccessfulRemoteDataObject$(process),
|
||||
});
|
||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(bitstream),
|
||||
});
|
||||
httpClient = jasmine.createSpyObj('httpClient', {
|
||||
get: observableOf('Script text'),
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||
declarations: [CollectionSourceControlsComponent, VarDirective],
|
||||
providers: [
|
||||
{provide: ScriptDataService, useValue: scriptDataService},
|
||||
{provide: ProcessDataService, useValue: processDataService},
|
||||
{provide: RequestService, useValue: requestService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: CollectionDataService, useValue: collectionService},
|
||||
{provide: HttpClient, useValue: httpClient},
|
||||
{provide: BitstreamDataService, useValue: bitstreamService}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CollectionSourceControlsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.isEnabled = true;
|
||||
comp.collection = collection;
|
||||
comp.shouldShow = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should', () => {
|
||||
expect(comp).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('testConfiguration', () => {
|
||||
it('should invoke a script and ping the resulting process until completed and show the resulting info', () => {
|
||||
comp.testConfiguration(contentSource);
|
||||
scheduler.flush();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('harvest', [
|
||||
{name: '-g', value: null},
|
||||
{name: '-a', value: contentSource.oaiSource},
|
||||
{name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)},
|
||||
], []);
|
||||
|
||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
||||
expect(bitstreamService.findByHref).toHaveBeenCalledWith(process._links.output.href);
|
||||
expect(notificationsService.info).toHaveBeenCalledWith(jasmine.anything() as any, 'Script text');
|
||||
});
|
||||
});
|
||||
describe('importNow', () => {
|
||||
it('should invoke a script that will start the harvest', () => {
|
||||
comp.importNow();
|
||||
scheduler.flush();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('harvest', [
|
||||
{name: '-r', value: null},
|
||||
{name: '-c', value: collection.uuid},
|
||||
], []);
|
||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('resetAndReimport', () => {
|
||||
it('should invoke a script that will start the harvest', () => {
|
||||
comp.resetAndReimport();
|
||||
scheduler.flush();
|
||||
|
||||
expect(scriptDataService.invoke).toHaveBeenCalledWith('harvest', [
|
||||
{name: '-o', value: null},
|
||||
{name: '-c', value: collection.uuid},
|
||||
], []);
|
||||
expect(processDataService.findById).toHaveBeenCalledWith(process.processId, false);
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('the controls', () => {
|
||||
it('should be shown when shouldShow is true', () => {
|
||||
comp.shouldShow = true;
|
||||
fixture.detectChanges();
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
expect(buttons.length).toEqual(3);
|
||||
});
|
||||
it('should be shown when shouldShow is false', () => {
|
||||
comp.shouldShow = false;
|
||||
fixture.detectChanges();
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
expect(buttons.length).toEqual(0);
|
||||
});
|
||||
it('should be disabled when isEnabled is false', () => {
|
||||
comp.shouldShow = true;
|
||||
comp.isEnabled = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
|
||||
expect(buttons[0].nativeElement.disabled).toBeTrue();
|
||||
expect(buttons[1].nativeElement.disabled).toBeTrue();
|
||||
expect(buttons[2].nativeElement.disabled).toBeTrue();
|
||||
});
|
||||
it('should be enabled when isEnabled is true', () => {
|
||||
comp.shouldShow = true;
|
||||
comp.isEnabled = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
|
||||
expect(buttons[0].nativeElement.disabled).toBeFalse();
|
||||
expect(buttons[1].nativeElement.disabled).toBeFalse();
|
||||
expect(buttons[2].nativeElement.disabled).toBeFalse();
|
||||
});
|
||||
it('should call the corresponding button when clicked', () => {
|
||||
spyOn(comp, 'testConfiguration');
|
||||
spyOn(comp, 'importNow');
|
||||
spyOn(comp, 'resetAndReimport');
|
||||
|
||||
comp.shouldShow = true;
|
||||
comp.isEnabled = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
|
||||
buttons[0].triggerEventHandler('click', null);
|
||||
expect(comp.testConfiguration).toHaveBeenCalled();
|
||||
|
||||
buttons[1].triggerEventHandler('click', null);
|
||||
expect(comp.importNow).toHaveBeenCalled();
|
||||
|
||||
buttons[2].triggerEventHandler('click', null);
|
||||
expect(comp.resetAndReimport).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
@@ -0,0 +1,231 @@
|
||||
import { Component, Input, OnDestroy } from '@angular/core';
|
||||
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { ContentSource } from '../../../../core/shared/content-source.model';
|
||||
import { ProcessDataService } from '../../../../core/data/processes/process-data.service';
|
||||
import {
|
||||
getAllCompletedRemoteData,
|
||||
getAllSucceededRemoteDataPayload,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../../../core/shared/operators';
|
||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
|
||||
import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../../../core/data/collection-data.service';
|
||||
import { Process } from '../../../../process-page/processes/process.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
|
||||
|
||||
/**
|
||||
* Component that contains the controls to run, reset and test the harvest
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-collection-source-controls',
|
||||
styleUrls: ['./collection-source-controls.component.scss'],
|
||||
templateUrl: './collection-source-controls.component.html',
|
||||
})
|
||||
export class CollectionSourceControlsComponent implements OnDestroy {
|
||||
|
||||
/**
|
||||
* Should the controls be enabled.
|
||||
*/
|
||||
@Input() isEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The current collection
|
||||
*/
|
||||
@Input() collection: Collection;
|
||||
|
||||
/**
|
||||
* Should the control section be shown
|
||||
*/
|
||||
@Input() shouldShow: boolean;
|
||||
|
||||
contentSource$: Observable<ContentSource>;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
testConfigRunning$ = new BehaviorSubject(false);
|
||||
importRunning$ = new BehaviorSubject(false);
|
||||
reImportRunning$ = new BehaviorSubject(false);
|
||||
|
||||
constructor(private scriptDataService: ScriptDataService,
|
||||
private processDataService: ProcessDataService,
|
||||
private requestService: RequestService,
|
||||
private notificationsService: NotificationsService,
|
||||
private collectionService: CollectionDataService,
|
||||
private translateService: TranslateService,
|
||||
private httpClient: HttpClient,
|
||||
private bitstreamService: BitstreamDataService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// ensure the contentSource gets updated after being set to stale
|
||||
this.contentSource$ = this.collectionService.findByHref(this.collection._links.self.href, false).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
switchMap((collection) => this.collectionService.getContentSource(collection.uuid, false)),
|
||||
getAllSucceededRemoteDataPayload()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the provided content source's configuration.
|
||||
* @param contentSource - The content source to be tested
|
||||
*/
|
||||
testConfiguration(contentSource) {
|
||||
this.testConfigRunning$.next(true);
|
||||
this.subs.push(this.scriptDataService.invoke('harvest', [
|
||||
{name: '-g', value: null},
|
||||
{name: '-a', value: contentSource.oaiSource},
|
||||
{name: '-i', value: new ContentSourceSetSerializer().Serialize(contentSource.oaiSetId)},
|
||||
], []).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
tap((rd) => {
|
||||
if (rd.hasFailed) {
|
||||
// show a notification when the script invocation fails
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.test.submit.error'));
|
||||
this.testConfigRunning$.next(false);
|
||||
}
|
||||
}),
|
||||
// filter out responses that aren't successful since the pinging of the process only needs to happen when the invocation was successful.
|
||||
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
||||
getAllCompletedRemoteData(),
|
||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
||||
map((rd) => rd.payload),
|
||||
hasValueOperator(),
|
||||
).subscribe((process: Process) => {
|
||||
if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() &&
|
||||
process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
// Ping the current process state every 5s
|
||||
setTimeout(() => {
|
||||
this.requestService.setStaleByHrefSubstring(process._links.self.href);
|
||||
}, 5000);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.test.failed'));
|
||||
this.testConfigRunning$.next(false);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) {
|
||||
this.bitstreamService.findByHref(process._links.output.href).pipe(getFirstSucceededRemoteDataPayload()).subscribe((bitstream) => {
|
||||
this.httpClient.get(bitstream._links.content.href, {responseType: 'text'}).subscribe((data: any) => {
|
||||
const output = data.replaceAll(new RegExp('.*\\@(.*)', 'g'), '$1')
|
||||
.replaceAll('The script has started', '')
|
||||
.replaceAll('The script has completed', '');
|
||||
this.notificationsService.info(this.translateService.get('collection.source.controls.test.completed'), output);
|
||||
});
|
||||
});
|
||||
this.testConfigRunning$.next(false);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the harvest for the current collection
|
||||
*/
|
||||
importNow() {
|
||||
this.importRunning$.next(true);
|
||||
this.subs.push(this.scriptDataService.invoke('harvest', [
|
||||
{name: '-r', value: null},
|
||||
{name: '-c', value: this.collection.uuid},
|
||||
], [])
|
||||
.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
tap((rd) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.import.submit.error'));
|
||||
this.importRunning$.next(false);
|
||||
} else {
|
||||
this.notificationsService.success(this.translateService.get('collection.source.controls.import.submit.success'));
|
||||
}
|
||||
}),
|
||||
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
||||
getAllCompletedRemoteData(),
|
||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
||||
map((rd) => rd.payload),
|
||||
hasValueOperator(),
|
||||
).subscribe((process) => {
|
||||
if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() &&
|
||||
process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
// Ping the current process state every 5s
|
||||
setTimeout(() => {
|
||||
this.requestService.setStaleByHrefSubstring(process._links.self.href);
|
||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||
}, 5000);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.import.failed'));
|
||||
this.importRunning$.next(false);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) {
|
||||
this.notificationsService.success(this.translateService.get('collection.source.controls.import.completed'));
|
||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||
this.importRunning$.next(false);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and reimport the current collection
|
||||
*/
|
||||
resetAndReimport() {
|
||||
this.reImportRunning$.next(true);
|
||||
this.subs.push(this.scriptDataService.invoke('harvest', [
|
||||
{name: '-o', value: null},
|
||||
{name: '-c', value: this.collection.uuid},
|
||||
], [])
|
||||
.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
tap((rd) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.reset.submit.error'));
|
||||
this.reImportRunning$.next(false);
|
||||
} else {
|
||||
this.notificationsService.success(this.translateService.get('collection.source.controls.reset.submit.success'));
|
||||
}
|
||||
}),
|
||||
filter((rd) => rd.hasSucceeded && hasValue(rd.payload)),
|
||||
switchMap((rd) => this.processDataService.findById(rd.payload.processId, false)),
|
||||
getAllCompletedRemoteData(),
|
||||
filter((rd) => !rd.isStale && (rd.hasSucceeded || rd.hasFailed)),
|
||||
map((rd) => rd.payload),
|
||||
hasValueOperator(),
|
||||
).subscribe((process) => {
|
||||
if (process.processStatus.toString() !== ProcessStatus[ProcessStatus.COMPLETED].toString() &&
|
||||
process.processStatus.toString() !== ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
// Ping the current process state every 5s
|
||||
setTimeout(() => {
|
||||
this.requestService.setStaleByHrefSubstring(process._links.self.href);
|
||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||
}, 5000);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()) {
|
||||
this.notificationsService.error(this.translateService.get('collection.source.controls.reset.failed'));
|
||||
this.reImportRunning$.next(false);
|
||||
}
|
||||
if (process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()) {
|
||||
this.notificationsService.success(this.translateService.get('collection.source.controls.reset.completed'));
|
||||
this.requestService.setStaleByHrefSubstring(this.collection._links.self.href);
|
||||
this.reImportRunning$.next(false);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach((sub) => {
|
||||
if (hasValue(sub)) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,57 +1,74 @@
|
||||
<div class="container-fluid">
|
||||
<div class="d-inline-block float-right">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
|
||||
<div *ngIf="contentSource" class="form-check mb-4">
|
||||
<input type="checkbox" class="form-check-input" id="externalSourceCheck" [checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
|
||||
<label class="form-check-label" for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||
</div>
|
||||
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||
<div class="d-inline-block float-right">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
[disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
|
||||
<div *ngIf="contentSource" class="form-check mb-4">
|
||||
<input type="checkbox" class="form-check-input" id="externalSourceCheck"
|
||||
[checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
|
||||
<label class="form-check-label"
|
||||
for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||
</div>
|
||||
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||
</div>
|
||||
<ds-form *ngIf="formGroup && contentSource && (contentSource?.harvestType !== harvestTypeNone)"
|
||||
[formId]="'collection-source-form-id'"
|
||||
[formGroup]="formGroup"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[displaySubmit]="false"
|
||||
[displayCancel]="false"
|
||||
(dfChange)="onChange($event)"
|
||||
(submitForm)="onSubmit()"
|
||||
(cancel)="onCancel()"></ds-form>
|
||||
<div class="container-fluid" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
|
||||
<div class="d-inline-block float-right">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ds-form *ngIf="formGroup && contentSource && (contentSource?.harvestType !== harvestTypeNone)"
|
||||
[formId]="'collection-source-form-id'"
|
||||
[formGroup]="formGroup"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[displaySubmit]="false"
|
||||
[displayCancel]="false"
|
||||
(dfChange)="onChange($event)"
|
||||
(submitForm)="onSubmit()"
|
||||
(cancel)="onCancel()"></ds-form>
|
||||
</div>
|
||||
<div class="container mt-2" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-inline-block float-right ml-1">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
[disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-source-controls
|
||||
[isEnabled]="!(hasChanges()|async)"
|
||||
[shouldShow]="contentSource?.harvestType !== harvestTypeNone"
|
||||
[collection]="(collectionRD$ |async)?.payload"
|
||||
>
|
||||
</ds-collection-source-controls>
|
||||
|
||||
|
@@ -62,7 +62,8 @@ describe('CollectionSourceComponent', () => {
|
||||
label: 'DSpace Intermediate Metadata',
|
||||
nameSpace: 'http://www.dspace.org/xmlns/dspace/dim'
|
||||
}
|
||||
]
|
||||
],
|
||||
_links: { self: { href: 'contentsource-selflink' } }
|
||||
});
|
||||
fieldUpdate = {
|
||||
field: contentSource,
|
||||
@@ -115,7 +116,7 @@ describe('CollectionSourceComponent', () => {
|
||||
updateContentSource: observableOf(contentSource),
|
||||
getHarvesterEndpoint: observableOf('harvester-endpoint')
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']);
|
||||
requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||
|
@@ -380,7 +380,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem
|
||||
switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)),
|
||||
take(1)
|
||||
).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint));
|
||||
|
||||
this.requestService.setStaleByHrefSubstring(this.contentSource._links.self.href);
|
||||
// Update harvester
|
||||
this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
|
@@ -9,6 +9,7 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
|
||||
import { CollectionSourceComponent } from './collection-source/collection-source.component';
|
||||
import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component';
|
||||
import { CollectionFormModule } from '../collection-form/collection-form.module';
|
||||
import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Collection page administrator functionality
|
||||
@@ -26,6 +27,8 @@ import { CollectionFormModule } from '../collection-form/collection-form.module'
|
||||
CollectionRolesComponent,
|
||||
CollectionCurateComponent,
|
||||
CollectionSourceComponent,
|
||||
|
||||
CollectionSourceControlsComponent,
|
||||
CollectionAuthorizationsComponent
|
||||
]
|
||||
})
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { CommunityListService, FlatNode } from './community-list-service';
|
||||
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, Observable, } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
|
@@ -42,7 +42,6 @@ import {
|
||||
UnsetUserAsIdleAction
|
||||
} from './auth.actions';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||
@@ -103,7 +102,7 @@ export class AuthService {
|
||||
*/
|
||||
public authenticate(user: string, password: string): Observable<AuthStatus> {
|
||||
// Attempt authenticating the user using the supplied credentials.
|
||||
const body = (`password=${Base64EncodeUrl(password)}&user=${Base64EncodeUrl(user)}`);
|
||||
const body = (`password=${encodeURIComponent(password)}&user=${encodeURIComponent(user)}`);
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
3
src/app/core/cache/builders/link.service.ts
vendored
3
src/app/core/cache/builders/link.service.ts
vendored
@@ -10,8 +10,7 @@ import {
|
||||
LinkDefinition
|
||||
} from './build-decorators';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from 'src/app/shared/remote-data.utils';
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { Observable } from 'rxjs';
|
||||
|
@@ -27,12 +27,7 @@ import { CommunityDataService } from './community-data.service';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import {
|
||||
ContentSourceRequest,
|
||||
FindListOptions,
|
||||
UpdateContentSourceRequest,
|
||||
RestRequest
|
||||
} from './request.models';
|
||||
import { ContentSourceRequest, FindListOptions, RestRequest, UpdateContentSourceRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { BitstreamDataService } from './bitstream-data.service';
|
||||
|
||||
@@ -84,16 +79,48 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all collections the user is authorized to submit to
|
||||
*
|
||||
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||
* @param entityType The entity type used to limit the returned collection
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
|
||||
* the response becomes stale
|
||||
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollectionByEntityType(
|
||||
query: string,
|
||||
entityType: string,
|
||||
options: FindListOptions = {},
|
||||
reRequestOnStale = true,
|
||||
...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorizedByEntityType';
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: [
|
||||
new RequestParam('query', query),
|
||||
new RequestParam('entityType', entityType)
|
||||
]
|
||||
});
|
||||
|
||||
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
|
||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all collections the user is authorized to submit to, by community
|
||||
*
|
||||
* @param communityId The community id
|
||||
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true,): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorizedByCommunity';
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: [
|
||||
@@ -102,7 +129,38 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
]
|
||||
});
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
return this.searchBy(searchHref, options, reRequestOnStale).pipe(
|
||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||
}
|
||||
/**
|
||||
* Get all collections the user is authorized to submit to, by community and has the metadata
|
||||
*
|
||||
* @param communityId The community id
|
||||
* @param entityType The entity type used to limit the returned collection
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
|
||||
* the response becomes stale
|
||||
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollectionByCommunityAndEntityType(
|
||||
communityId: string,
|
||||
entityType: string,
|
||||
options: FindListOptions = {},
|
||||
reRequestOnStale = true,
|
||||
...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorizedByCommunityAndEntityType';
|
||||
const searchParams = [
|
||||
new RequestParam('uuid', communityId),
|
||||
new RequestParam('entityType', entityType)
|
||||
];
|
||||
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: searchParams
|
||||
});
|
||||
|
||||
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
|
||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||
}
|
||||
|
||||
@@ -138,7 +196,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
* Get the collection's content harvester
|
||||
* @param collectionId
|
||||
*/
|
||||
getContentSource(collectionId: string): Observable<RemoteData<ContentSource>> {
|
||||
getContentSource(collectionId: string, useCachedVersionIfAvailable = true): Observable<RemoteData<ContentSource>> {
|
||||
const href$ = this.getHarvesterEndpoint(collectionId).pipe(
|
||||
isNotEmptyOperator(),
|
||||
take(1)
|
||||
@@ -146,7 +204,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
|
||||
href$.subscribe((href: string) => {
|
||||
const request = new ContentSourceRequest(this.requestService.generateRequestId(), href);
|
||||
this.requestService.send(request, true);
|
||||
this.requestService.send(request, useCachedVersionIfAvailable);
|
||||
});
|
||||
|
||||
return this.rdbService.buildSingle<ContentSource>(href$);
|
||||
@@ -208,10 +266,20 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link RemoteData} of {@link Collection} that is the owing collection of the given item
|
||||
* Returns {@link RemoteData} of {@link Collection} that is the owning collection of the given item
|
||||
* @param item Item we want the owning collection of
|
||||
*/
|
||||
findOwningCollectionFor(item: Item): Observable<RemoteData<Collection>> {
|
||||
return this.findByHref(item._links.owningCollection.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of mapped collections for the given item.
|
||||
* @param item Item for which the mapped collections should be retrieved.
|
||||
* @param findListOptions Pagination and search options.
|
||||
*/
|
||||
findMappedCollectionsFor(item: Item, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
return this.findAllByHref(item._links.mappedCollections.href, findListOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,13 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { switchMap, take, map } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { ItemType } from '../shared/item-relationships/item-type.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../shared/operators';
|
||||
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||
import { RelationshipTypeService } from './relationship-type.service';
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ export class EntityTypeService extends DataService<ItemType> {
|
||||
/**
|
||||
* Check whether a given entity type is the left type of a given relationship type, as an observable boolean
|
||||
* @param relationshipType the relationship type for which to check whether the given entity type is the left type
|
||||
* @param entityType the entity type for which to check whether it is the left type of the given relationship type
|
||||
* @param itemType the entity type for which to check whether it is the left type of the given relationship type
|
||||
*/
|
||||
isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable<boolean> {
|
||||
|
||||
@@ -67,6 +68,73 @@ export class EntityTypeService extends DataService<ItemType> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of entity types for which there is at least one collection in which the user is authorized to submit
|
||||
*
|
||||
* @param {FindListOptions} options
|
||||
*/
|
||||
getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
|
||||
const searchHref = 'findAllByAuthorizedCollection';
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to verify if there are one or more entities available
|
||||
*/
|
||||
hasMoreThanOneAuthorized(): Observable<boolean> {
|
||||
const findListOptions: FindListOptions = {
|
||||
elementsPerPage: 2,
|
||||
currentPage: 1
|
||||
};
|
||||
return this.getAllAuthorizedRelationshipType(findListOptions).pipe(
|
||||
map((result: RemoteData<PaginatedList<ItemType>>) => {
|
||||
let output: boolean;
|
||||
if (result.payload) {
|
||||
output = ( result.payload.page.length > 1 );
|
||||
} else {
|
||||
output = false;
|
||||
}
|
||||
return output;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns a list of entity types for which there is at least one collection
|
||||
* in which the user is authorized to submit supported by at least one external data source provider
|
||||
*
|
||||
* @param {FindListOptions} options
|
||||
*/
|
||||
getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
|
||||
const searchHref = 'findAllByAuthorizedExternalSource';
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to verify if there are one or more entities available. To use with external source import.
|
||||
*/
|
||||
hasMoreThanOneAuthorizedImport(): Observable<boolean> {
|
||||
const findListOptions: FindListOptions = {
|
||||
elementsPerPage: 2,
|
||||
currentPage: 1
|
||||
};
|
||||
return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe(
|
||||
map((result: RemoteData<PaginatedList<ItemType>>) => {
|
||||
let output: boolean;
|
||||
if (result.payload) {
|
||||
output = ( result.payload.page.length > 1 );
|
||||
} else {
|
||||
output = false;
|
||||
}
|
||||
return output;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed relationship types for an entity type
|
||||
* @param entityTypeId
|
||||
|
@@ -7,7 +7,7 @@ import { PostRequest } from './request.models';
|
||||
import { Registration } from '../shared/registration.model';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
describe('EpersonRegistrationService', () => {
|
||||
|
@@ -14,6 +14,7 @@ export enum FeatureID {
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanDownload = 'canDownload',
|
||||
CanRequestACopy = 'canRequestACopy',
|
||||
CanManageVersions = 'canManageVersions',
|
||||
CanManageBitstreamBundles = 'canManageBitstreamBundles',
|
||||
CanManageRelationships = 'canManageRelationships',
|
||||
@@ -21,4 +22,7 @@ export enum FeatureID {
|
||||
CanManagePolicies = 'canManagePolicies',
|
||||
CanMakePrivate = 'canMakePrivate',
|
||||
CanMove = 'canMove',
|
||||
CanEditVersion = 'canEditVersion',
|
||||
CanDeleteVersion = 'canDeleteVersion',
|
||||
CanCreateVersion = 'canCreateVersion',
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ import { FindListOptions } from './request.models';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
|
||||
import { LICENSE } from '../shared/license.resource-type';
|
||||
|
@@ -31,7 +31,7 @@ describe('ItemDataService', () => {
|
||||
},
|
||||
removeByHrefSubstring(href: string) {
|
||||
// Do nothing
|
||||
}
|
||||
},
|
||||
}) as RequestService;
|
||||
const rdbService = getMockRemoteDataBuildService();
|
||||
|
||||
@@ -184,4 +184,14 @@ describe('ItemDataService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cache is invalidated', () => {
|
||||
beforeEach(() => {
|
||||
service = initTestService();
|
||||
});
|
||||
it('should call setStaleByHrefSubstring', () => {
|
||||
service.invalidateItemCache('uuid');
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('item/uuid');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -59,6 +59,7 @@ export class ItemDataService extends DataService<Item> {
|
||||
* Get the endpoint for browsing items
|
||||
* (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
|
||||
* @param {FindListOptions} options
|
||||
* @param linkPath
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
|
||||
@@ -287,4 +288,13 @@ export class ItemDataService extends DataService<Item> {
|
||||
switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cache of the item
|
||||
* @param itemUUID
|
||||
*/
|
||||
invalidateItemCache(itemUUID: string) {
|
||||
this.requestService.setStaleByHrefSubstring('item/' + itemUUID);
|
||||
}
|
||||
|
||||
}
|
||||
|
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
95
src/app/core/data/item-request-data.service.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ItemRequestDataService } from './item-request-data.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { ItemRequest } from '../shared/item-request.model';
|
||||
import { PostRequest } from './request.models';
|
||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
|
||||
describe('ItemRequestDataService', () => {
|
||||
let service: ItemRequestDataService;
|
||||
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let halService: HALEndpointService;
|
||||
|
||||
const restApiEndpoint = 'rest/api/endpoint/';
|
||||
const requestId = 'request-id';
|
||||
let itemRequest: ItemRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
itemRequest = Object.assign(new ItemRequest(), {
|
||||
token: 'item-request-token',
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestId,
|
||||
send: '',
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildFromRequestUUID: createSuccessfulRemoteDataObject$(itemRequest),
|
||||
});
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: observableOf(restApiEndpoint),
|
||||
});
|
||||
|
||||
service = new ItemRequestDataService(requestService, rdbService, null, null, halService, null, null, null);
|
||||
});
|
||||
|
||||
describe('requestACopy', () => {
|
||||
it('should send a POST request containing the provided item request', (done) => {
|
||||
service.requestACopy(itemRequest).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(new PostRequest(requestId, restApiEndpoint, itemRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('grant', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
});
|
||||
|
||||
it('should send a PUT request containing the correct properties', (done) => {
|
||||
service.grant(itemRequest.token, email, true).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
method: RestRequestMethod.PUT,
|
||||
body: JSON.stringify({
|
||||
acceptRequest: true,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess: true,
|
||||
}),
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deny', () => {
|
||||
let email: RequestCopyEmail;
|
||||
|
||||
beforeEach(() => {
|
||||
email = new RequestCopyEmail('subject', 'message');
|
||||
});
|
||||
|
||||
it('should send a PUT request containing the correct properties', (done) => {
|
||||
service.deny(itemRequest.token, email).subscribe(() => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||
method: RestRequestMethod.PUT,
|
||||
body: JSON.stringify({
|
||||
acceptRequest: false,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess: false,
|
||||
}),
|
||||
}));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
131
src/app/core/data/item-request-data.service.ts
Normal file
131
src/app/core/data/item-request-data.service.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, find, map } from 'rxjs/operators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getFirstCompletedRemoteData, sendRequest } from '../shared/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PostRequest, PutRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { ItemRequest } from '../shared/item-request.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { DataService } from './data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint
|
||||
*/
|
||||
@Injectable(
|
||||
{
|
||||
providedIn: 'root',
|
||||
}
|
||||
)
|
||||
export class ItemRequestDataService extends DataService<ItemRequest> {
|
||||
|
||||
protected linkPath = 'itemrequests';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<ItemRequest>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getItemRequestEndpoint(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for an {@link ItemRequest} by their token
|
||||
* @param token
|
||||
*/
|
||||
getItemRequestEndpointByToken(token: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => `${href}/${token}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a copy of an item
|
||||
* @param itemRequest
|
||||
*/
|
||||
requestACopy(itemRequest: ItemRequest): Observable<RemoteData<ItemRequest>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const href$ = this.getItemRequestEndpoint();
|
||||
|
||||
href$.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PostRequest(requestId, href, itemRequest);
|
||||
this.requestService.send(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
return this.rdbService.buildFromRequestUUID<ItemRequest>(requestId).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
*/
|
||||
deny(token: string, email: RequestCopyEmail): Observable<RemoteData<ItemRequest>> {
|
||||
return this.process(token, email, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||
*/
|
||||
grant(token: string, email: RequestCopyEmail, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||
return this.process(token, email, true, suggestOpenAccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the request of an item
|
||||
* @param token Token of the {@link ItemRequest}
|
||||
* @param email Email to send back to the user requesting the item
|
||||
* @param grant Grant or deny the request (true = grant, false = deny)
|
||||
* @param suggestOpenAccess Whether or not to suggest the item to become open access
|
||||
*/
|
||||
process(token: string, email: RequestCopyEmail, grant: boolean, suggestOpenAccess = false): Observable<RemoteData<ItemRequest>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
this.getItemRequestEndpointByToken(token).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((endpointURL: string) => {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new PutRequest(requestId, endpointURL, JSON.stringify({
|
||||
acceptRequest: grant,
|
||||
responseMessage: email.message,
|
||||
subject: email.subject,
|
||||
suggestOpenAccess,
|
||||
}), options);
|
||||
}),
|
||||
sendRequest(this.requestService)).subscribe();
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
}
|
@@ -15,13 +15,14 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ItemType } from '../shared/item-relationships/item-type.model';
|
||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type';
|
||||
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators';
|
||||
import { DataService } from './data.service';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { ItemDataService } from './item-data.service';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestService } from './request.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* Check if one side of a RelationshipType is the ItemType with the given label
|
||||
@@ -120,4 +121,33 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search of the given RelationshipType if has the given itemTypes on its left and right sides.
|
||||
* Returns an observable of the given RelationshipType if it matches, null if it doesn't
|
||||
*
|
||||
* @param type The RelationshipType to check
|
||||
*/
|
||||
searchByEntityType(type: string,useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<RelationshipType>[]): Observable<PaginatedList<RelationshipType>> {
|
||||
|
||||
return this.searchBy(
|
||||
'byEntityType',
|
||||
{
|
||||
searchParams: [
|
||||
{
|
||||
fieldName: 'type',
|
||||
fieldValue: type
|
||||
},
|
||||
{
|
||||
fieldName: 'size',
|
||||
fieldValue: 100
|
||||
},
|
||||
]
|
||||
}, useCachedVersionIfAvailable,reRequestOnStale,...linksToFollow).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
) as Observable<PaginatedList<RelationshipType>>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -469,4 +469,58 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
update(object: Relationship): Observable<RemoteData<Relationship>> {
|
||||
return this.put(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch isn't supported on the relationship endpoint, so use put instead.
|
||||
*
|
||||
* @param typeId the relationship type id to apply as a filter to the returned relationships
|
||||
* @param itemUuid The uuid of the item to be checked on the side defined by relationshipLabel
|
||||
* @param relationshipLabel the name of the relation as defined from the side of the itemUuid
|
||||
* @param arrayOfItemIds The uuid of the items to be found on the other side of returned relationships
|
||||
*/
|
||||
searchByItemsAndType(typeId: string,itemUuid: string,relationshipLabel: string, arrayOfItemIds: string[] ): Observable<RemoteData<PaginatedList<Relationship>>> {
|
||||
|
||||
const searchParams = [
|
||||
{
|
||||
fieldName: 'typeId',
|
||||
fieldValue: typeId
|
||||
},
|
||||
{
|
||||
fieldName: 'focusItem',
|
||||
fieldValue: itemUuid
|
||||
},
|
||||
{
|
||||
fieldName: 'relationshipLabel',
|
||||
fieldValue: relationshipLabel
|
||||
},
|
||||
{
|
||||
fieldName: 'size',
|
||||
fieldValue: arrayOfItemIds.length
|
||||
},
|
||||
{
|
||||
fieldName: 'embed',
|
||||
fieldValue: 'leftItem'
|
||||
},
|
||||
{
|
||||
fieldName: 'embed',
|
||||
fieldValue: 'rightItem'
|
||||
},
|
||||
];
|
||||
|
||||
arrayOfItemIds.forEach( (itemId) => {
|
||||
searchParams.push(
|
||||
{
|
||||
fieldName: 'relatedItem',
|
||||
fieldValue: itemId
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return this.searchBy(
|
||||
'byItemsAndType',
|
||||
{
|
||||
searchParams: searchParams
|
||||
}) as Observable<RemoteData<PaginatedList<Relationship>>>;
|
||||
|
||||
}
|
||||
}
|
||||
|
181
src/app/core/data/version-data.service.spec.ts
Normal file
181
src/app/core/data/version-data.service.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { HrefOnlyDataService } from './href-only-data.service';
|
||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { VersionDataService } from './version-data.service';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { VersionHistory } from '../shared/version-history.model';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
|
||||
describe('VersionDataService test', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: VersionDataService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let hrefOnlyDataService: HrefOnlyDataService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
id: '1234-1234',
|
||||
uuid: '1234-1234',
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||
|
||||
const versionHistory = Object.assign(new VersionHistory(), {
|
||||
id: '1',
|
||||
draftVersion: true,
|
||||
});
|
||||
|
||||
const mockVersion: Version = Object.assign(new Version(), {
|
||||
item: createSuccessfulRemoteDataObject$(item),
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||
version: 1,
|
||||
});
|
||||
const mockVersionRD = createSuccessfulRemoteDataObject(mockVersion);
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/versioning/versions`;
|
||||
const findByIdRequestURL = `https://rest.api/rest/api/versioning/versions/${mockVersion.id}`;
|
||||
const findByIdRequestURL$ = observableOf(findByIdRequestURL);
|
||||
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const comparatorEntry = {} as any;
|
||||
const store = {} as Store<CoreState>;
|
||||
const pageInfo = new PageInfo();
|
||||
|
||||
function initTestService() {
|
||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||
return new VersionDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparatorEntry
|
||||
);
|
||||
}
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: endpointURL })
|
||||
});
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: observableOf(responseCacheEntry),
|
||||
getByUUID: observableOf(responseCacheEntry),
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: hot('(a|)', {
|
||||
a: mockVersionRD
|
||||
})
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
|
||||
spyOn((service as any), 'findByHref').and.callThrough();
|
||||
spyOn((service as any), 'getIDHrefObs').and.returnValue(findByIdRequestURL$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service = null;
|
||||
});
|
||||
|
||||
describe('getHistoryFromVersion', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
scheduler.schedule(() => service.getHistoryFromVersion(mockVersion, true, true));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).findByHref).toHaveBeenCalledWith(findByIdRequestURL$, true, true, followLink('versionhistory'));
|
||||
});
|
||||
|
||||
it('should return a VersionHistory', () => {
|
||||
const result = service.getHistoryFromVersion(mockVersion, true, true);
|
||||
const expected = cold('(a|)', {
|
||||
a: versionHistory
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return an EMPTY observable when version is not given', () => {
|
||||
const result = service.getHistoryFromVersion(null);
|
||||
const expected = cold('|');
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistoryIdFromVersion', () => {
|
||||
it('should return the version history id', () => {
|
||||
spyOn((service as any), 'getHistoryFromVersion').and.returnValue(observableOf(versionHistory));
|
||||
|
||||
const result = service.getHistoryIdFromVersion(mockVersion);
|
||||
const expected = cold('(a|)', {
|
||||
a: versionHistory.id
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -10,10 +10,14 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION } from '../shared/version.resource-type';
|
||||
import { VersionHistory } from '../shared/version-history.model';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Version object
|
||||
@@ -36,9 +40,29 @@ export class VersionDataService extends DataService<Version> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing versions
|
||||
* Get the version history for the given version
|
||||
* @param version
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
*/
|
||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
getHistoryFromVersion(version: Version, useCachedVersionIfAvailable = false, reRequestOnStale = true): Observable<VersionHistory> {
|
||||
return isNotEmpty(version) ? this.findById(version.id, useCachedVersionIfAvailable, reRequestOnStale, followLink('versionhistory')).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((res: Version) => res.versionhistory),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
) : EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the version history for the given version
|
||||
* @param version
|
||||
*/
|
||||
getHistoryIdFromVersion(version: Version): Observable<string> {
|
||||
return this.getHistoryFromVersion(version).pipe(
|
||||
map((versionHistory: VersionHistory) => versionHistory.id),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,14 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { VersionDataService } from './version-data.service';
|
||||
import { fakeAsync, waitForAsync } from '@angular/core/testing';
|
||||
import { VersionHistory } from '../shared/version-history.model';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { of } from 'rxjs';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
|
||||
const url = 'fake-url';
|
||||
|
||||
@@ -16,9 +24,97 @@ describe('VersionHistoryDataService', () => {
|
||||
let notificationsService: any;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let versionService: VersionDataService;
|
||||
let versionService: SpyObj<VersionDataService>;
|
||||
let halService: any;
|
||||
|
||||
const versionHistoryId = 'version-history-id';
|
||||
const versionHistoryDraftId = 'version-history-draft-id';
|
||||
const version1Id = 'version-1-id';
|
||||
const version2Id = 'version-1-id';
|
||||
const item1Uuid = 'item-1-uuid';
|
||||
const item2Uuid = 'item-2-uuid';
|
||||
const versionHistory = Object.assign(new VersionHistory(), {
|
||||
id: versionHistoryId,
|
||||
draftVersion: false,
|
||||
});
|
||||
const versionHistoryDraft = Object.assign(new VersionHistory(), {
|
||||
id: versionHistoryDraftId,
|
||||
draftVersion: true,
|
||||
});
|
||||
const version1 = Object.assign(new Version(), {
|
||||
id: version1Id,
|
||||
version: 1,
|
||||
created: new Date(2020, 1, 1),
|
||||
summary: 'first version',
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||
_links: {
|
||||
self: {
|
||||
href: 'version1-url',
|
||||
},
|
||||
},
|
||||
});
|
||||
const version2 = Object.assign(new Version(), {
|
||||
id: version2Id,
|
||||
version: 2,
|
||||
summary: 'second version',
|
||||
created: new Date(2020, 1, 2),
|
||||
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||
_links: {
|
||||
self: {
|
||||
href: 'version2-url',
|
||||
},
|
||||
},
|
||||
});
|
||||
const versions = [version1, version2];
|
||||
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions));
|
||||
const item1 = Object.assign(new Item(), {
|
||||
uuid: item1Uuid,
|
||||
handle: '123456789/1',
|
||||
version: createSuccessfulRemoteDataObject$(version1),
|
||||
_links: {
|
||||
self: {
|
||||
href: '/items/' + item2Uuid,
|
||||
}
|
||||
}
|
||||
});
|
||||
const item2 = Object.assign(new Item(), {
|
||||
uuid: item2Uuid,
|
||||
handle: '123456789/2',
|
||||
version: createSuccessfulRemoteDataObject$(version2),
|
||||
_links: {
|
||||
self: {
|
||||
href: '/items/' + item2Uuid,
|
||||
}
|
||||
}
|
||||
});
|
||||
const items = [item1, item2];
|
||||
version1.item = createSuccessfulRemoteDataObject$(item1);
|
||||
version2.item = createSuccessfulRemoteDataObject$(item2);
|
||||
|
||||
/**
|
||||
* Create a VersionHistoryDataService used for testing
|
||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
||||
*/
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList'),
|
||||
buildFromRequestUUID: jasmine.createSpy('buildFromRequestUUID'),
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
versionService = jasmine.createSpyObj('objectCache', {
|
||||
findByHref: jasmine.createSpy('findByHref'),
|
||||
findAllByHref: jasmine.createSpy('findAllByHref'),
|
||||
getHistoryFromVersion: jasmine.createSpy('getHistoryFromVersion'),
|
||||
});
|
||||
halService = new HALEndpointServiceStub(url);
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
});
|
||||
@@ -35,24 +131,70 @@ describe('VersionHistoryDataService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a VersionHistoryDataService used for testing
|
||||
* @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional)
|
||||
*/
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList')
|
||||
describe('when getVersions is called', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
service.getVersions(versionHistoryId);
|
||||
}));
|
||||
it('findAllByHref should have been called', () => {
|
||||
expect(versionService.findAllByHref).toHaveBeenCalled();
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
versionService = jasmine.createSpyObj('objectCache', {
|
||||
findAllByHref: jasmine.createSpy('findAllByHref')
|
||||
});
|
||||
halService = new HALEndpointServiceStub(url);
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
});
|
||||
|
||||
describe('when getBrowseEndpoint is called', () => {
|
||||
it('should return the correct value', () => {
|
||||
service.getBrowseEndpoint().subscribe((res) => {
|
||||
expect(res).toBe(url + '/versionhistories');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getVersionsEndpoint is called', () => {
|
||||
it('should return the correct value', () => {
|
||||
service.getVersionsEndpoint(versionHistoryId).subscribe((res) => {
|
||||
expect(res).toBe(url + '/versions');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cache is invalidated', () => {
|
||||
it('should call setStaleByHrefSubstring', () => {
|
||||
service.invalidateVersionHistoryCache(versionHistoryId);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('versioning/versionhistories/' + versionHistoryId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLatest$', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
spyOn(service, 'getLatestVersion$').and.returnValue(of(version2));
|
||||
}));
|
||||
it('should return false for version1', () => {
|
||||
service.isLatest$(version1).subscribe((res) => {
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should return true for version2', () => {
|
||||
service.isLatest$(version2).subscribe((res) => {
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDraftVersion$', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$<Version>(version1));
|
||||
}));
|
||||
it('should return false if draftVersion is false', fakeAsync(() => {
|
||||
versionService.getHistoryFromVersion.and.returnValue(of(versionHistory));
|
||||
service.hasDraftVersion$('href').subscribe((res) => {
|
||||
expect(res).toBeFalse();
|
||||
});
|
||||
}));
|
||||
it('should return true if draftVersion is true', fakeAsync(() => {
|
||||
versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft));
|
||||
service.hasDraftVersion$('href').subscribe((res) => {
|
||||
expect(res).toBeTrue();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
service = new VersionHistoryDataService(requestService, rdbService, null, objectCache, halService, notificationsService, versionService, null, null);
|
||||
}
|
||||
});
|
||||
|
@@ -8,19 +8,30 @@ import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { FindListOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { Version } from '../shared/version.model';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { VERSION_HISTORY } from '../shared/version-history.resource-type';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { VersionDataService } from './version-data.service';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getRemoteDataPayload,
|
||||
sendRequest
|
||||
} from '../shared/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { hasValueOperator } from '../../shared/empty.util';
|
||||
import { Item } from '../shared/item.model';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the VersionHistory object
|
||||
@@ -79,4 +90,129 @@ export class VersionHistoryDataService extends DataService<VersionHistory> {
|
||||
|
||||
return this.versionDataService.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new version for an item
|
||||
* @param itemHref the item for which create a new version
|
||||
* @param summary the summary of the new version
|
||||
*/
|
||||
createVersion(itemHref: string, summary: string): Observable<RemoteData<Version>> {
|
||||
const requestOptions: HttpOptions = Object.create({});
|
||||
let requestHeaders = new HttpHeaders();
|
||||
requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list');
|
||||
requestOptions.headers = requestHeaders;
|
||||
|
||||
return this.halService.getEndpoint(this.versionsEndpoint).pipe(
|
||||
take(1),
|
||||
map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`),
|
||||
map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)),
|
||||
sendRequest(this.requestService),
|
||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||
getFirstCompletedRemoteData()
|
||||
) as Observable<RemoteData<Version>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version in a version history
|
||||
* @param versionHistory
|
||||
*/
|
||||
getLatestVersionFromHistory$(versionHistory: VersionHistory): Observable<Version> {
|
||||
|
||||
// Pagination options to fetch a single version on the first page (this is the latest version in the history)
|
||||
const latestVersionOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'item-newest-version-options',
|
||||
currentPage: 1,
|
||||
pageSize: 1
|
||||
});
|
||||
|
||||
const latestVersionSearch = new PaginatedSearchOptions({pagination: latestVersionOptions});
|
||||
|
||||
return this.getVersions(versionHistory.id, latestVersionSearch, false, true, followLink('item')).pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
filter((versions) => versions.page.length > 0),
|
||||
map((versions) => versions.page[0])
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version (return null if the specified version is null)
|
||||
* @param version
|
||||
*/
|
||||
getLatestVersion$(version: Version): Observable<Version> {
|
||||
// retrieve again version, including with versionHistory
|
||||
return version.id ? this.versionDataService.findById(version.id, false, true, followLink('versionhistory')).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((res) => res.versionhistory),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)),
|
||||
) : of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given version is the latest (return null if `version` is null)
|
||||
* @param version
|
||||
* @returns `true` if the specified version is the latest one, `false` otherwise, or `null` if the specified version is null
|
||||
*/
|
||||
isLatest$(version: Version): Observable<boolean> {
|
||||
return version ? this.getLatestVersion$(version).pipe(
|
||||
take(1),
|
||||
switchMap((latestVersion) => of(version.version === latestVersion.version))
|
||||
) : of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a worskpace item exists in the version history (return null if there is no version history)
|
||||
* @param versionHref the href of the version
|
||||
* @returns `true` if a workspace item exists, `false` otherwise, or `null` if a version history does not exist
|
||||
*/
|
||||
hasDraftVersion$(versionHref: string): Observable<boolean> {
|
||||
return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((res) => {
|
||||
if (res.hasSucceeded && !res.hasNoContent) {
|
||||
return of(res).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((version) => this.versionDataService.getHistoryFromVersion(version)),
|
||||
map((versionHistory) => versionHistory ? versionHistory.draftVersion : false),
|
||||
);
|
||||
} else {
|
||||
return of(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item of the latest version in a version history
|
||||
* @param versionHistory
|
||||
*/
|
||||
getLatestVersionItemFromHistory$(versionHistory: VersionHistory): Observable<Item> {
|
||||
return this.getLatestVersionFromHistory$(versionHistory).pipe(
|
||||
switchMap((newLatestVersion) => newLatestVersion.item),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item of the latest version from any version in the version history
|
||||
* @param version
|
||||
*/
|
||||
getVersionHistoryFromVersion$(version: Version): Observable<VersionHistory> {
|
||||
return this.versionDataService.getHistoryIdFromVersion(version).pipe(
|
||||
take(1),
|
||||
switchMap((res) => this.findById(res)),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cache of the version history
|
||||
* @param versionHistoryID
|
||||
*/
|
||||
invalidateVersionHistoryCache(versionHistoryID: string) {
|
||||
this.requestService.setStaleByHrefSubstring('versioning/versionhistories/' + versionHistoryID);
|
||||
}
|
||||
}
|
||||
|
@@ -56,11 +56,11 @@ describe('EPersonDataService', () => {
|
||||
}
|
||||
|
||||
function init() {
|
||||
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
|
||||
restEndpointURL = 'https://rest.api/dspace-spring-rest/api/eperson';
|
||||
epersonsEndpoint = `${restEndpointURL}/epersons`;
|
||||
epeople = [EPersonMock, EPersonMock2];
|
||||
epeople$ = createSuccessfulRemoteDataObject$(createPaginatedList([epeople]));
|
||||
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ });
|
||||
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/dspace-spring-rest/api/eperson/epersons': epeople$ });
|
||||
halService = new HALEndpointServiceStub(restEndpointURL);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
@@ -21,6 +21,7 @@ describe('LocaleService test suite', () => {
|
||||
let spyOnSet;
|
||||
let authService;
|
||||
let routeService;
|
||||
let document;
|
||||
|
||||
authService = jasmine.createSpyObj('AuthService', {
|
||||
isAuthenticated: jasmine.createSpy('isAuthenticated'),
|
||||
@@ -43,6 +44,7 @@ describe('LocaleService test suite', () => {
|
||||
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||
{ provide: AuthService, userValue: authService },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Document, useValue: document },
|
||||
]
|
||||
});
|
||||
}));
|
||||
@@ -52,7 +54,8 @@ describe('LocaleService test suite', () => {
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
routeService = TestBed.inject(RouteService);
|
||||
window = new NativeWindowRef();
|
||||
service = new LocaleService(window, cookieService, translateService, authService, routeService);
|
||||
document = { documentElement: { lang: 'en' } };
|
||||
service = new LocaleService(window, cookieService, translateService, authService, routeService, document);
|
||||
serviceAsAny = service;
|
||||
spyOnGet = spyOn(cookieService, 'get');
|
||||
spyOnSet = spyOn(cookieService, 'set');
|
||||
@@ -114,6 +117,12 @@ describe('LocaleService test suite', () => {
|
||||
expect(translateService.use).toHaveBeenCalledWith('es');
|
||||
expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('es');
|
||||
});
|
||||
|
||||
it('should set the current language on the html tag', () => {
|
||||
spyOn(service, 'getCurrentLanguageCode').and.returnValue('es');
|
||||
service.setCurrentLanguageCode();
|
||||
expect((service as any).document.documentElement.lang).toEqual('es');
|
||||
});
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
|
@@ -10,6 +10,7 @@ import { combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { RouteService } from '../services/route.service';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
export const LANG_COOKIE = 'dsLanguage';
|
||||
|
||||
@@ -38,7 +39,9 @@ export class LocaleService {
|
||||
protected cookie: CookieService,
|
||||
protected translate: TranslateService,
|
||||
protected authService: AuthService,
|
||||
protected routeService: RouteService) {
|
||||
protected routeService: RouteService,
|
||||
@Inject(DOCUMENT) private document: any
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,6 +151,7 @@ export class LocaleService {
|
||||
}
|
||||
this.translate.use(lang);
|
||||
this.saveLanguageCodeToCookie(lang);
|
||||
this.document.documentElement.lang = lang;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -10,7 +10,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { difference } from '../../shared/object.util';
|
||||
import { isNumeric } from 'rxjs/internal-compatibility';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
|
26
src/app/core/shared/content-source-set-serializer.spec.ts
Normal file
26
src/app/core/shared/content-source-set-serializer.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ContentSourceSetSerializer } from './content-source-set-serializer';
|
||||
|
||||
describe('ContentSourceSetSerializer', () => {
|
||||
let serializer: ContentSourceSetSerializer;
|
||||
|
||||
beforeEach(() => {
|
||||
serializer = new ContentSourceSetSerializer();
|
||||
});
|
||||
|
||||
describe('Serialize', () => {
|
||||
it('should return all when the value is empty', () => {
|
||||
expect(serializer.Serialize('')).toEqual('all');
|
||||
});
|
||||
it('should return the value when it is not empty', () => {
|
||||
expect(serializer.Serialize('test-value')).toEqual('test-value');
|
||||
});
|
||||
});
|
||||
describe('Deserialize', () => {
|
||||
it('should return an empty value when the value is \'all\'', () => {
|
||||
expect(serializer.Deserialize('all')).toEqual('');
|
||||
});
|
||||
it('should return the value when it is not \'all\'', () => {
|
||||
expect(serializer.Deserialize('test-value')).toEqual('test-value');
|
||||
});
|
||||
});
|
||||
});
|
31
src/app/core/shared/content-source-set-serializer.ts
Normal file
31
src/app/core/shared/content-source-set-serializer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { isEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Serializer to create convert the 'all' value supported by the server to an empty string and vice versa.
|
||||
*/
|
||||
export class ContentSourceSetSerializer {
|
||||
|
||||
/**
|
||||
* Method to serialize a setId
|
||||
* @param {string} setId
|
||||
* @returns {string} the provided set ID, unless when an empty set ID is provided. In that case, 'all' will be returned.
|
||||
*/
|
||||
Serialize(setId: string): any {
|
||||
if (isEmpty(setId)) {
|
||||
return 'all';
|
||||
}
|
||||
return setId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to deserialize a setId
|
||||
* @param {string} setId
|
||||
* @returns {string} the provided set ID. When 'all' is provided, an empty set ID will be returned.
|
||||
*/
|
||||
Deserialize(setId: string): string {
|
||||
if (setId === 'all') {
|
||||
return '';
|
||||
}
|
||||
return setId;
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { autoserializeAs, deserializeAs, deserialize } from 'cerialize';
|
||||
import { autoserializeAs, deserialize, deserializeAs, serializeAs } from 'cerialize';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { MetadataConfig } from './metadata-config.model';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
@@ -6,6 +6,7 @@ import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { CONTENT_SOURCE } from './content-source.resource-type';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { ContentSourceSetSerializer } from './content-source-set-serializer';
|
||||
|
||||
/**
|
||||
* The type of content harvesting used
|
||||
@@ -49,7 +50,8 @@ export class ContentSource extends CacheableObject {
|
||||
/**
|
||||
* OAI Specific set ID
|
||||
*/
|
||||
@autoserializeAs('oai_set_id')
|
||||
@deserializeAs(new ContentSourceSetSerializer(), 'oai_set_id')
|
||||
@serializeAs(new ContentSourceSetSerializer(), 'oai_set_id')
|
||||
oaiSetId: string;
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,30 @@ export class ContentSource extends CacheableObject {
|
||||
*/
|
||||
metadataConfigs: MetadataConfig[];
|
||||
|
||||
/**
|
||||
* The current harvest status
|
||||
*/
|
||||
@autoserializeAs('harvest_status')
|
||||
harvestStatus: string;
|
||||
|
||||
/**
|
||||
* The last's harvest start time
|
||||
*/
|
||||
@autoserializeAs('harvest_start_time')
|
||||
harvestStartTime: string;
|
||||
|
||||
/**
|
||||
* When the collection was last harvested
|
||||
*/
|
||||
@autoserializeAs('last_harvested')
|
||||
lastHarvested: string;
|
||||
|
||||
/**
|
||||
* The current harvest message
|
||||
*/
|
||||
@autoserializeAs('harvest_message')
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this ContentSource
|
||||
*/
|
||||
|
@@ -1,10 +1,15 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { EXTERNAL_SOURCE } from './external-source.resource-type';
|
||||
import { HALLink } from './hal-link.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ITEM_TYPE } from './item-relationships/item-type.resource-type';
|
||||
import { ItemType } from './item-relationships/item-type.model';
|
||||
|
||||
/**
|
||||
* Model class for an external source
|
||||
@@ -38,6 +43,13 @@ export class ExternalSource extends CacheableObject {
|
||||
@autoserialize
|
||||
hierarchical: boolean;
|
||||
|
||||
/**
|
||||
* The list of entity types that are compatible with this external source
|
||||
* Will be undefined unless the entityTypes {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(ITEM_TYPE, true)
|
||||
entityTypes?: Observable<RemoteData<PaginatedList<ItemType>>>;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this ExternalSource
|
||||
*/
|
||||
@@ -45,5 +57,6 @@ export class ExternalSource extends CacheableObject {
|
||||
_links: {
|
||||
self: HALLink;
|
||||
entries: HALLink;
|
||||
entityTypes: HALLink;
|
||||
};
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { map, take } from 'rxjs/operators';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Provides utility methods to save files on the client-side.
|
||||
|
@@ -6,5 +6,9 @@ import { ResourceType } from '../resource-type';
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
|
||||
export const ITEM_TYPE = new ResourceType('entitytype');
|
||||
|
||||
/**
|
||||
* The unset entity type
|
||||
*/
|
||||
export const NONE_ENTITY_TYPE = 'none';
|
||||
|
90
src/app/core/shared/item-request.model.ts
Normal file
90
src/app/core/shared/item-request.model.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { ResourceType } from './resource-type';
|
||||
import { ITEM_REQUEST } from './item-request.resource-type';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { HALLink } from './hal-link.model';
|
||||
|
||||
/**
|
||||
* Model class for an ItemRequest
|
||||
*/
|
||||
@typedObject
|
||||
export class ItemRequest implements CacheableObject {
|
||||
static type = ITEM_REQUEST;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* opaque string which uniquely identifies this request
|
||||
*/
|
||||
@autoserialize
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* true if the request is for all bitstreams of the item.
|
||||
*/
|
||||
@autoserialize
|
||||
allfiles: boolean;
|
||||
/**
|
||||
* email address of the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestEmail: string;
|
||||
/**
|
||||
* Human-readable name of the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestName: string;
|
||||
/**
|
||||
* arbitrary message provided by the person requesting the files.
|
||||
*/
|
||||
@autoserialize
|
||||
requestMessage: string;
|
||||
/**
|
||||
* date that the request was recorded.
|
||||
*/
|
||||
@autoserialize
|
||||
requestDate: string;
|
||||
/**
|
||||
* true if the request has been granted.
|
||||
*/
|
||||
@autoserialize
|
||||
acceptRequest: boolean;
|
||||
/**
|
||||
* date that the request was granted or denied.
|
||||
*/
|
||||
@autoserialize
|
||||
decisionDate: string;
|
||||
/**
|
||||
* date on which the request is considered expired.
|
||||
*/
|
||||
@autoserialize
|
||||
expires: string;
|
||||
/**
|
||||
* UUID of the requested Item.
|
||||
*/
|
||||
@autoserialize
|
||||
itemId: string;
|
||||
/**
|
||||
* UUID of the requested bitstream.
|
||||
*/
|
||||
@autoserialize
|
||||
bitstreamId: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this ItemRequest
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
item: HALLink;
|
||||
bitstream: HALLink;
|
||||
};
|
||||
|
||||
}
|
9
src/app/core/shared/item-request.resource-type.ts
Normal file
9
src/app/core/shared/item-request.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for ItemRequest.
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ITEM_REQUEST = new ResourceType('itemrequest');
|
@@ -1,4 +1,4 @@
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { autoserialize, Serialize, Deserialize } from 'cerialize';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { isUndefined } from '../../shared/empty.util';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models';
|
||||
import { Metadata } from './metadata.utils';
|
||||
|
||||
|
@@ -26,7 +26,7 @@ import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../s
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { SearchConfig } from './search-filters/search-config.model';
|
||||
import { SearchService } from './search.service';
|
||||
import { of } from 'rxjs/internal/observable/of';
|
||||
import { of } from 'rxjs';
|
||||
import { PaginationService } from '../../pagination/pagination.service';
|
||||
|
||||
/**
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { LinkService } from '../../cache/builders/link.service';
|
||||
import { PaginatedList } from '../../data/paginated-list.model';
|
||||
import { ResponseParsingService } from '../../data/parsing.service';
|
||||
@@ -13,7 +13,7 @@ import { DSpaceObject } from '../dspace-object.model';
|
||||
import { GenericConstructor } from '../generic-constructor';
|
||||
import { HALEndpointService } from '../hal-endpoint.service';
|
||||
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||
import { hasValue, isEmpty, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
|
||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { SearchOptions } from '../../../shared/search/search-options.model';
|
||||
import { SearchFilterConfig } from '../../../shared/search/search-filter-config.model';
|
||||
import { SearchResponseParsingService } from '../../data/search-response-parsing.service';
|
||||
@@ -21,16 +21,11 @@ import { SearchObjects } from '../../../shared/search/search-objects.model';
|
||||
import { FacetValueResponseParsingService } from '../../data/facet-value-response-parsing.service';
|
||||
import { FacetConfigResponseParsingService } from '../../data/facet-config-response-parsing.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { Community } from '../community.model';
|
||||
import { CommunityDataService } from '../../data/community-data.service';
|
||||
import { ViewMode } from '../view-mode.model';
|
||||
import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
|
||||
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||
import {
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getRemoteDataPayload
|
||||
} from '../operators';
|
||||
import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../operators';
|
||||
import { RouteService } from '../../services/route.service';
|
||||
import { SearchResult } from '../../../shared/search/search-result.model';
|
||||
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
|
||||
@@ -395,48 +390,6 @@ export class SearchService implements OnDestroy {
|
||||
return this.rdb.buildFromHref(href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a list of DSpaceObjects that can be used as a scope, based on the current scope
|
||||
* @param {string} scopeId UUID of the current scope, if the scope is empty, the repository wide scopes will be returned
|
||||
* @returns {Observable<DSpaceObject[]>} Emits a list of DSpaceObjects which represent possible scopes
|
||||
*/
|
||||
getScopes(scopeId?: string): Observable<DSpaceObject[]> {
|
||||
|
||||
if (isEmpty(scopeId)) {
|
||||
const top: Observable<Community[]> = this.communityService.findTop({ elementsPerPage: 9999 }).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map(
|
||||
(communities: RemoteData<PaginatedList<Community>>) => communities.payload.page
|
||||
)
|
||||
);
|
||||
return top;
|
||||
}
|
||||
|
||||
const scopeObject: Observable<RemoteData<DSpaceObject>> = this.dspaceObjectService.findById(scopeId).pipe(getFirstSucceededRemoteData());
|
||||
const scopeList: Observable<DSpaceObject[]> = scopeObject.pipe(
|
||||
switchMap((dsoRD: RemoteData<DSpaceObject>) => {
|
||||
if ((dsoRD.payload as any).type === Community.type.value) {
|
||||
const community: Community = dsoRD.payload as Community;
|
||||
this.linkService.resolveLinks(community, followLink('subcommunities'), followLink('collections'));
|
||||
return observableCombineLatest([
|
||||
community.subcommunities.pipe(getFirstCompletedRemoteData()),
|
||||
community.collections.pipe(getFirstCompletedRemoteData())
|
||||
]).pipe(
|
||||
map(([subCommunities, collections]) => {
|
||||
/*if this is a community, we also need to show the direct children*/
|
||||
return [community, ...subCommunities.payload.page, ...collections.payload.page];
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return observableOf([dsoRD.payload]);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
return scopeList;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the current view mode based on the current URL
|
||||
* @returns {Observable<ViewMode>} The current view mode
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as uuidv4 from 'uuid/v4';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class UUIDService {
|
||||
|
@@ -22,6 +22,7 @@ export class VersionHistory extends DSpaceObject {
|
||||
_links: {
|
||||
self: HALLink;
|
||||
versions: HALLink;
|
||||
draftVersion: HALLink;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,24 @@ export class VersionHistory extends DSpaceObject {
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The summary of this Version History
|
||||
*/
|
||||
@autoserialize
|
||||
summary: string;
|
||||
|
||||
/**
|
||||
* The name of the submitter of this Version History
|
||||
*/
|
||||
@autoserialize
|
||||
submitterName: string;
|
||||
|
||||
/**
|
||||
* Whether exist a workspace item
|
||||
*/
|
||||
@autoserialize
|
||||
draftVersion: boolean;
|
||||
|
||||
/**
|
||||
* The list of versions within this history
|
||||
*/
|
||||
|
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workflowitem-data.service.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkflowItem } from './models/workflowitem.model';
|
||||
|
||||
describe('WorkflowItemDataService test', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: WorkflowItemDataService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let hrefOnlyDataService: HrefOnlyDataService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
id: '1234-1234',
|
||||
uuid: '1234-1234',
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||
const wsi = Object.assign(new WorkflowItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
|
||||
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
||||
const searchRequestURL$ = observableOf(searchRequestURL);
|
||||
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const comparatorEntry = {} as any;
|
||||
const store = {} as Store<CoreState>;
|
||||
const pageInfo = new PageInfo();
|
||||
|
||||
function initTestService() {
|
||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||
return new WorkflowItemDataService(
|
||||
comparatorEntry,
|
||||
halService,
|
||||
http,
|
||||
notificationsService,
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
store
|
||||
);
|
||||
}
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: endpointURL })
|
||||
});
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: observableOf(responseCacheEntry),
|
||||
getByUUID: observableOf(responseCacheEntry),
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: hot('a|', {
|
||||
a: wsiRD
|
||||
})
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
|
||||
spyOn((service as any), 'findByHref').and.callThrough();
|
||||
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service = null;
|
||||
});
|
||||
|
||||
describe('findByItem', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
||||
const expected = cold('a|', {
|
||||
a: wsiRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -9,7 +9,7 @@ import { DataService } from '../data/data.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { WorkflowItem } from './models/workflowitem.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { DeleteByIDRequest } from '../data/request.models';
|
||||
import { DeleteByIDRequest, FindListOptions } from '../data/request.models';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
@@ -19,6 +19,9 @@ import { hasValue } from '../../shared/empty.util';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workflow items endpoint.
|
||||
@@ -27,6 +30,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
@dataService(WorkflowItem.type)
|
||||
export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
protected linkPath = 'workflowitems';
|
||||
protected searchByItemLinkPath = 'item';
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
@@ -86,4 +90,23 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the WorkflowItem object found through the UUID of an item
|
||||
*
|
||||
* @param uuid The uuid of the item
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param options The {@link FindListOptions} object
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||
const findListOptions = new FindListOptions();
|
||||
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
}
|
||||
|
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal file
150
src/app/core/submission/workspaceitem-data.service.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { HrefOnlyDataService } from '../data/href-only-data.service';
|
||||
import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
|
||||
describe('WorkspaceitemDataService test', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: WorkspaceitemDataService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let hrefOnlyDataService: HrefOnlyDataService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
id: '1234-1234',
|
||||
uuid: '1234-1234',
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const itemRD = createSuccessfulRemoteDataObject(item);
|
||||
const wsi = Object.assign(new WorkspaceItem(), { item: observableOf(itemRD), id: '1234', uuid: '1234' });
|
||||
const wsiRD = createSuccessfulRemoteDataObject(wsi);
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/submission/workspaceitems`;
|
||||
const searchRequestURL = `https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234`;
|
||||
const searchRequestURL$ = observableOf(searchRequestURL);
|
||||
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const comparatorEntry = {} as any;
|
||||
const store = {} as Store<CoreState>;
|
||||
const pageInfo = new PageInfo();
|
||||
|
||||
function initTestService() {
|
||||
hrefOnlyDataService = getMockHrefOnlyDataService();
|
||||
return new WorkspaceitemDataService(
|
||||
comparatorEntry,
|
||||
halService,
|
||||
http,
|
||||
notificationsService,
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
store
|
||||
);
|
||||
}
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: endpointURL })
|
||||
});
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: observableOf(responseCacheEntry),
|
||||
getByUUID: observableOf(responseCacheEntry),
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: hot('a|', {
|
||||
a: wsiRD
|
||||
})
|
||||
});
|
||||
|
||||
service = initTestService();
|
||||
|
||||
spyOn((service as any), 'findByHref').and.callThrough();
|
||||
spyOn((service as any), 'getSearchByHref').and.returnValue(searchRequestURL$);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service = null;
|
||||
});
|
||||
|
||||
describe('findByItem', () => {
|
||||
it('should proxy the call to DataService.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).findByHref).toHaveBeenCalledWith(searchRequestURL$, true, true);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<WorkspaceItem> for the search', () => {
|
||||
const result = service.findByItem('1234-1234', true, true, pageInfo);
|
||||
const expected = cold('a|', {
|
||||
a: wsiRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -12,6 +12,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workspaceitems endpoint.
|
||||
@@ -20,6 +25,7 @@ import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
@dataService(WorkspaceItem.type)
|
||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||
protected linkPath = 'workspaceitems';
|
||||
protected searchByItemLinkPath = 'item';
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||
@@ -33,4 +39,22 @@ export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the WorkspaceItem object found through the UUID of an item
|
||||
*
|
||||
* @param uuid The uuid of the item
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param options The {@link FindListOptions} object
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||
const findListOptions = new FindListOptions();
|
||||
findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))];
|
||||
const href$ = this.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow);
|
||||
return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ClaimedTaskDataService } from './claimed-task-data.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
@@ -10,7 +10,7 @@ import { CoreState } from '../core.reducers';
|
||||
import { PoolTaskDataService } from './pool-task-data.service';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
|
@@ -17,7 +17,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
|
@@ -8,11 +8,10 @@ import {
|
||||
HttpResponse,
|
||||
HttpXsrfTokenExtractor
|
||||
} from '@angular/common/http';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { tap, catchError } from 'rxjs/operators';
|
||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import { throwError } from 'rxjs';
|
||||
|
||||
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
|
||||
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
|
||||
|
@@ -28,6 +28,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { JournalComponent } from './journal.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
|
||||
let comp: JournalComponent;
|
||||
let fixture: ComponentFixture<JournalComponent>;
|
||||
@@ -86,6 +87,7 @@ describe('JournalComponent', () => {
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: RouteService, useValue: {} }
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -21,6 +21,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
describe('UploadBistreamComponent', () => {
|
||||
let comp: UploadBitstreamComponent;
|
||||
@@ -76,7 +77,8 @@ describe('UploadBistreamComponent', () => {
|
||||
const restEndpoint = 'fake-rest-endpoint';
|
||||
const mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
getBitstreamsEndpoint: observableOf(restEndpoint),
|
||||
createBundle: createSuccessfulRemoteDataObject$(createdBundle)
|
||||
createBundle: createSuccessfulRemoteDataObject$(createdBundle),
|
||||
getBundles: createSuccessfulRemoteDataObject$([bundle])
|
||||
});
|
||||
const bundleService = jasmine.createSpyObj('bundleService', {
|
||||
getBitstreamsEndpoint: observableOf(restEndpoint),
|
||||
@@ -92,6 +94,22 @@ describe('UploadBistreamComponent', () => {
|
||||
removeByHrefSubstring: {}
|
||||
});
|
||||
|
||||
|
||||
describe('on init', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
createUploadBitstreamTestingModule({
|
||||
bundle: bundle.id
|
||||
});
|
||||
}));
|
||||
beforeEach(() => {
|
||||
loadFixtureAndComp();
|
||||
});
|
||||
it('should initialize the bundles', () => {
|
||||
expect(comp.bundlesRD$).toBeDefined();
|
||||
getTestScheduler().expectObservable(comp.bundlesRD$).toBe('(a|)', {a: createSuccessfulRemoteDataObject([bundle])});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a file is uploaded', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
createUploadBitstreamTestingModule({});
|
||||
|
@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { UploaderOptions } from '../../../shared/uploader/uploader-options.model';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
@@ -108,9 +108,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
|
||||
this.itemId = this.route.snapshot.params.id;
|
||||
this.entityType = this.route.snapshot.params['entity-type'];
|
||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
|
||||
this.bundlesRD$ = this.itemRD$.pipe(
|
||||
switchMap((itemRD: RemoteData<Item>) => itemRD.payload.bundles)
|
||||
);
|
||||
this.bundlesRD$ = this.itemService.getBundles(this.itemId);
|
||||
this.selectedBundleId = this.route.snapshot.queryParams.bundle;
|
||||
if (isNotEmpty(this.selectedBundleId)) {
|
||||
this.bundleService.findById(this.selectedBundleId).pipe(
|
||||
|
@@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<td>
|
||||
<div class="metadata-field">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.key?.split('.').join('.​')}}</span>
|
||||
<span >{{metadata?.key?.split('.').join('.​')}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<ds-validation-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||
<ds-validation-suggestions [disable]="fieldUpdate.changeType != 1" [suggestions]="(metadataFieldSuggestions | async)"
|
||||
[(ngModel)]="metadata.key"
|
||||
[url]="this.url"
|
||||
[metadata]="this.metadata"
|
||||
|
@@ -463,4 +463,43 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('canEditMetadataField', () => {
|
||||
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
||||
beforeEach(() => {
|
||||
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('can edit metadata field', () => {
|
||||
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
|
||||
.componentInstance.disable;
|
||||
expect(disabledMetadataField).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
||||
beforeEach(() => {
|
||||
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('can edit metadata field', () => {
|
||||
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
|
||||
.componentInstance.disable;
|
||||
expect(disabledMetadataField).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('when the fieldUpdate\'s changeType is currently UPDATE', () => {
|
||||
beforeEach(() => {
|
||||
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('can edit metadata field', () => {
|
||||
const disabledMetadataField = fixture.debugElement.query(By.css('ds-validation-suggestions'))
|
||||
.componentInstance.disable;
|
||||
expect(disabledMetadataField).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,66 +1,69 @@
|
||||
<div class="item-metadata">
|
||||
<div class="button-row top d-flex mb-2">
|
||||
<button class="mr-auto btn btn-success"
|
||||
(click)="add()"><i
|
||||
class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
||||
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
||||
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
||||
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
</tr>
|
||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||
ds-edit-in-place-field
|
||||
[fieldUpdate]="updateValue || {}"
|
||||
[url]="url"
|
||||
[ngClass]="{
|
||||
<div class="button-row top d-flex mb-2">
|
||||
<button class="mr-auto btn btn-success"
|
||||
(click)="add()"><i
|
||||
class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-responsive table-striped table-bordered"
|
||||
*ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span id="fieldName">{{'item.edit.metadata.headers.field' | translate}}</span></th>
|
||||
<th><span id="fieldValue">{{'item.edit.metadata.headers.value' | translate}}</span></th>
|
||||
<th class="text-center"><span id="fieldLang">{{'item.edit.metadata.headers.language' | translate}}</span></th>
|
||||
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||
ds-edit-in-place-field
|
||||
[fieldUpdate]="updateValue || {}"
|
||||
[url]="url"
|
||||
[ngClass]="{
|
||||
'table-warning': updateValue.changeType === 0,
|
||||
'table-danger': updateValue.changeType === 2,
|
||||
'table-success': updateValue.changeType === 1
|
||||
}">
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||
</div>
|
||||
<div class="button-row bottom">
|
||||
<div class="mt-2 float-right">
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-row bottom">
|
||||
<div class="mt-2 float-right">
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<h5>
|
||||
{{getRelationshipMessageKey() | async | translate}}
|
||||
<button class="ml-2 btn btn-success" (click)="openLookup()">
|
||||
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||
</button>
|
||||
|
@@ -22,6 +22,7 @@ import { HostWindowService } from '../../../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
||||
|
||||
let comp: EditRelationshipListComponent;
|
||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||
@@ -33,6 +34,7 @@ let relationshipService;
|
||||
let selectableListService;
|
||||
let paginationService;
|
||||
let hostWindowService;
|
||||
const relationshipTypeService = {};
|
||||
|
||||
const url = 'http://test-url.com/test-url';
|
||||
|
||||
@@ -57,6 +59,7 @@ describe('EditRelationshipListComponent', () => {
|
||||
comp.itemType = entityType;
|
||||
comp.url = url;
|
||||
comp.relationshipType = relationshipType;
|
||||
comp.hasChanges = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
@@ -181,6 +184,7 @@ describe('EditRelationshipListComponent', () => {
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: HostWindowService, useValue: hostWindowService },
|
||||
{ provide: RelationshipTypeService, useValue: relationshipTypeService },
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
@@ -275,6 +279,24 @@ describe('EditRelationshipListComponent', () => {
|
||||
expect(label).toEqual('isAuthorOfPublication');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('changes managment for add buttons', () => {
|
||||
|
||||
it('should show enabled add buttons', () => {
|
||||
const element = de.query(By.css('.btn-success'));
|
||||
expect(element.nativeElement?.disabled).toBeFalse();
|
||||
});
|
||||
|
||||
it('after hash changes changed', () => {
|
||||
comp.hasChanges = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
const element = de.query(By.css('.btn-success'));
|
||||
expect(element.nativeElement?.disabled).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,14 +1,9 @@
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
from as observableFrom
|
||||
} from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, from as observableFrom, BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates,
|
||||
@@ -16,39 +11,28 @@ import {
|
||||
} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import {
|
||||
defaultIfEmpty,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
take,
|
||||
startWith,
|
||||
toArray,
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { hasValue, hasValueOperator, hasNoValue } from '../../../../shared/empty.util';
|
||||
import { defaultIfEmpty, map, mergeMap, startWith, switchMap, take, tap, toArray } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, hasValueOperator } from '../../../../shared/empty.util';
|
||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||
import {
|
||||
getRemoteDataPayload,
|
||||
getAllSucceededRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getAllSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
||||
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { SearchResult } from '../../../../shared/search/search-result.model';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-relationship-list',
|
||||
@@ -79,6 +63,16 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Input() relationshipType: RelationshipType;
|
||||
|
||||
/**
|
||||
* If updated information has changed
|
||||
*/
|
||||
@Input() hasChanges!: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The event emmiter to submit the new information
|
||||
*/
|
||||
@Output() submit: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Observable that emits the left and right item type of {@link relationshipType} simultaneously.
|
||||
*/
|
||||
@@ -138,10 +132,12 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
modalRef: NgbModalRef;
|
||||
|
||||
|
||||
constructor(
|
||||
protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected linkService: LinkService,
|
||||
protected relationshipService: RelationshipService,
|
||||
protected relationshipTypeService: RelationshipTypeService,
|
||||
protected modalService: NgbModal,
|
||||
protected paginationService: PaginationService,
|
||||
protected selectableListService: SelectableListService,
|
||||
@@ -207,104 +203,174 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
const modalComp: DsDynamicLookupRelationModalComponent = this.modalRef.componentInstance;
|
||||
modalComp.repeatable = true;
|
||||
modalComp.isEditRelationship = true;
|
||||
modalComp.listId = this.listId;
|
||||
modalComp.item = this.item;
|
||||
modalComp.relationshipType = this.relationshipType;
|
||||
modalComp.currentItemIsLeftItem$ = this.currentItemIsLeftItem$;
|
||||
modalComp.toAdd = [];
|
||||
modalComp.toRemove = [];
|
||||
modalComp.isPending = false;
|
||||
|
||||
this.item.owningCollection.pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((collection: Collection) => {
|
||||
modalComp.collection = collection;
|
||||
});
|
||||
|
||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
||||
selectableObjects.forEach((searchResult) => {
|
||||
const relatedItem: Item = searchResult.indexableObject;
|
||||
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||
.subscribe((identifiables) => {
|
||||
identifiables.forEach((identifiable) =>
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.url, identifiable.uuid)
|
||||
);
|
||||
if (identifiables.length === 0) {
|
||||
this.relationshipService.getNameVariant(this.listId, relatedItem.uuid)
|
||||
.subscribe((nameVariant) => {
|
||||
const update = {
|
||||
uuid: this.relationshipType.id + '-' + relatedItem.uuid,
|
||||
nameVariant,
|
||||
type: this.relationshipType,
|
||||
relatedItem,
|
||||
} as RelationshipIdentifiable;
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||
});
|
||||
}
|
||||
|
||||
this.loading$.next(true);
|
||||
// emit the last page again to trigger a fieldupdates refresh
|
||||
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||
});
|
||||
const foundIndex = modalComp.toRemove.findIndex( el => el.uuid === relatedItem.uuid);
|
||||
|
||||
if (foundIndex !== -1) {
|
||||
modalComp.toRemove.splice(foundIndex,1);
|
||||
} else {
|
||||
|
||||
this.getRelationFromId(relatedItem)
|
||||
.subscribe((relationship: Relationship) => {
|
||||
if (!relationship ) {
|
||||
modalComp.toAdd.push(searchResult);
|
||||
} else {
|
||||
const foundIndexRemove = modalComp.toRemove.findIndex( el => el.indexableObject.uuid === relatedItem.uuid);
|
||||
if (foundIndexRemove !== -1) {
|
||||
modalComp.toRemove.splice(foundIndexRemove,1);
|
||||
}
|
||||
}
|
||||
|
||||
this.loading$.next(true);
|
||||
// emit the last page again to trigger a fieldupdates refresh
|
||||
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
modalComp.deselect = (...selectableObjects: SearchResult<Item>[]) => {
|
||||
selectableObjects.forEach((searchResult) => {
|
||||
const relatedItem: Item = searchResult.indexableObject;
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.relationshipType.id + '-' + relatedItem.uuid);
|
||||
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||
.subscribe((identifiables) =>
|
||||
identifiables.forEach((identifiable) =>
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, identifiable)
|
||||
)
|
||||
);
|
||||
|
||||
const foundIndex = modalComp.toAdd.findIndex( el => el.indexableObject.uuid === relatedItem.uuid);
|
||||
|
||||
if (foundIndex !== -1) {
|
||||
modalComp.toAdd.splice(foundIndex,1);
|
||||
} else {
|
||||
modalComp.toRemove.push(searchResult);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
modalComp.submitEv = () => {
|
||||
|
||||
const subscriptions = [];
|
||||
|
||||
modalComp.toAdd.forEach((searchResult: SearchResult<Item>) => {
|
||||
const relatedItem = searchResult.indexableObject;
|
||||
subscriptions.push(this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe(
|
||||
map((nameVariant) => {
|
||||
const update = {
|
||||
uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid,
|
||||
nameVariant,
|
||||
type: this.relationshipType,
|
||||
relatedItem,
|
||||
} as RelationshipIdentifiable;
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||
return update;
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
this.loading$.next(true);
|
||||
// emit the last page again to trigger a fieldupdates refresh
|
||||
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||
modalComp.toRemove.forEach( (searchResult) => {
|
||||
subscriptions.push(this.relationshipService.getNameVariant(this.listId, searchResult.indexableObjectuuid).pipe(
|
||||
switchMap((nameVariant) => {
|
||||
return this.getRelationFromId(searchResult.indexableObject).pipe(
|
||||
map( (relationship: Relationship) => {
|
||||
const update = {
|
||||
uuid: relationship.id,
|
||||
nameVariant,
|
||||
type: this.relationshipType,
|
||||
relationship,
|
||||
} as RelationshipIdentifiable;
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update);
|
||||
return update;
|
||||
})
|
||||
);
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
observableCombineLatest(subscriptions).subscribe( (res) => {
|
||||
// Wait until the states changes since there are multiple items
|
||||
setTimeout( () => {
|
||||
this.submit.emit();
|
||||
},1000);
|
||||
|
||||
modalComp.isPending = true;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
modalComp.discardEv = () => {
|
||||
modalComp.toAdd.forEach( (searchResult) => {
|
||||
this.selectableListService.deselectSingle(this.listId,searchResult);
|
||||
});
|
||||
|
||||
modalComp.toRemove.forEach( (searchResult) => {
|
||||
this.selectableListService.selectSingle(this.listId,searchResult);
|
||||
});
|
||||
|
||||
modalComp.toAdd = [];
|
||||
modalComp.toRemove = [];
|
||||
};
|
||||
|
||||
this.relatedEntityType$
|
||||
.pipe(take(1))
|
||||
.subscribe((relatedEntityType) => {
|
||||
modalComp.relationshipOptions = Object.assign(
|
||||
new RelationshipOptions(), {
|
||||
relationshipType: relatedEntityType.label,
|
||||
// filter: this.getRelationshipMessageKey(),
|
||||
searchConfiguration: relatedEntityType.label.toLowerCase(),
|
||||
nameVariants: true,
|
||||
nameVariants: 'true',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.selectableListService.deselectAll(this.listId);
|
||||
this.updates$.pipe(
|
||||
switchMap((updates) =>
|
||||
Object.values(updates).length > 0 ?
|
||||
observableCombineLatest(
|
||||
Object.values(updates)
|
||||
.filter((update) => update.changeType !== FieldChangeType.REMOVE)
|
||||
.map((update) => {
|
||||
const field = update.field as RelationshipIdentifiable;
|
||||
if (field.relationship) {
|
||||
return this.getRelatedItem(field.relationship);
|
||||
} else {
|
||||
return observableOf(field.relatedItem);
|
||||
}
|
||||
})
|
||||
) : observableOf([])
|
||||
),
|
||||
take(1),
|
||||
map((items) => items.map((item) => {
|
||||
const searchResult = new ItemSearchResult();
|
||||
searchResult.indexableObject = item;
|
||||
searchResult.hitHighlights = {};
|
||||
return searchResult;
|
||||
})),
|
||||
).subscribe((items) => {
|
||||
this.selectableListService.select(this.listId, items);
|
||||
});
|
||||
}
|
||||
|
||||
getRelationFromId(relatedItem) {
|
||||
return this.currentItemIsLeftItem$.pipe(
|
||||
take(1),
|
||||
switchMap( isLeft => {
|
||||
let apiCall;
|
||||
if (isLeft) {
|
||||
apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.leftwardType ,[relatedItem.id] ).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
} else {
|
||||
apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.rightwardType ,[relatedItem.id] ).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
}
|
||||
|
||||
return apiCall.pipe(
|
||||
map( (res: PaginatedList<Relationship>) => res.page[0])
|
||||
);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the existing field updates regarding a relationship with a given item
|
||||
* @param relatedItem The item for which to get the existing field updates
|
||||
*/
|
||||
private getFieldUpdatesForRelatedItem(relatedItem: Item): Observable<RelationshipIdentifiable[]> {
|
||||
|
||||
return this.updates$.pipe(
|
||||
take(1),
|
||||
map((updates) => Object.values(updates)
|
||||
@@ -316,11 +382,34 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
identifiables.map((identifiable) => this.getRelatedItem(identifiable.relationship))
|
||||
).pipe(
|
||||
defaultIfEmpty([]),
|
||||
map((relatedItems) =>
|
||||
identifiables.filter((identifiable, index) => relatedItems[index].uuid === relatedItem.uuid)
|
||||
map((relatedItems) => {
|
||||
return identifiables.filter( (identifiable, index) => {
|
||||
return relatedItems[index].uuid === relatedItem.uuid;
|
||||
});
|
||||
}
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given item is related with the item we are editing relationships
|
||||
* @param relatedItem The item for which to get the existing field updates
|
||||
*/
|
||||
private getIsRelatedItem(relatedItem: Item): Observable<boolean> {
|
||||
|
||||
return this.currentItemIsLeftItem$.pipe(
|
||||
take(1),
|
||||
map( isLeft => {
|
||||
if (isLeft) {
|
||||
const listOfRelatedItems = this.item.allMetadataValues( 'relation.' + this.relationshipType.leftwardType );
|
||||
return !!listOfRelatedItems.find( (uuid) => uuid === relatedItem.uuid );
|
||||
} else {
|
||||
const listOfRelatedItems = this.item.allMetadataValues( 'relation.' + this.relationshipType.rightwardType );
|
||||
return !!listOfRelatedItems.find( (uuid) => uuid === relatedItem.uuid );
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -337,6 +426,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
// store the left and right type of the relationship in a single observable
|
||||
this.relationshipLeftAndRightType$ = observableCombineLatest([
|
||||
this.relationshipType.leftType,
|
||||
@@ -373,6 +463,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// initialize the pagination options
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = `er${this.relationshipType.id}`;
|
||||
|
@@ -79,7 +79,7 @@ export class EditRelationshipComponent implements OnChanges {
|
||||
* Sets the current relationship based on the fieldUpdate input field
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
if (this.relationship) {
|
||||
if (this.relationship && (!!this.relationship.leftItem || !!this.relationship.rightItem)) {
|
||||
this.leftItem$ = this.relationship.leftItem.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
|
@@ -27,6 +27,8 @@
|
||||
[item]="item"
|
||||
[itemType]="entityType$ | async"
|
||||
[relationshipType]="relationshipType"
|
||||
[hasChanges] = hasChanges()
|
||||
(submit) = submit()
|
||||
></ds-edit-relationship-list>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -25,6 +25,8 @@ import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { ItemRelationshipsComponent } from './item-relationships.component';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
|
||||
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock';
|
||||
|
||||
let comp: any;
|
||||
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
||||
@@ -46,6 +48,7 @@ const notificationsService = jasmine.createSpyObj('notificationsService',
|
||||
}
|
||||
);
|
||||
const router = new RouterStub();
|
||||
let relationshipTypeService;
|
||||
let routeStub;
|
||||
let itemService;
|
||||
|
||||
@@ -178,6 +181,13 @@ describe('ItemRelationshipsComponent', () => {
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
relationshipTypeService = jasmine.createSpyObj('searchByEntityType',
|
||||
{
|
||||
searchByEntityType: observableOf(relationshipTypes)
|
||||
}
|
||||
);
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService',
|
||||
{
|
||||
removeByHrefSubstring: {},
|
||||
@@ -210,6 +220,7 @@ describe('ItemRelationshipsComponent', () => {
|
||||
{ provide: EntityTypeService, useValue: entityTypeService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RelationshipTypeService, useValue: relationshipTypeService },
|
||||
ChangeDetectorRef
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
@@ -255,4 +266,22 @@ describe('ItemRelationshipsComponent', () => {
|
||||
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('discard', () => {
|
||||
beforeEach(() => {
|
||||
comp.item.firstMetadataValue = (info) => {
|
||||
return 'Publication';
|
||||
};
|
||||
fixture.detectChanges();
|
||||
comp.initializeUpdates();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('it should call relationshipTypeService.searchByEntityType', () => {
|
||||
expect(relationshipTypeService.searchByEntityType).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -6,13 +6,8 @@ import {
|
||||
FieldUpdates,
|
||||
RelationshipIdentifiable,
|
||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
of as observableOf,
|
||||
zip as observableZip
|
||||
} from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip, Observable } from 'rxjs';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
@@ -32,6 +27,9 @@ import { FieldChangeType } from '../../../core/data/object-updates/object-update
|
||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-relationships',
|
||||
@@ -65,7 +63,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||
public objectCache: ObjectCacheService,
|
||||
public requestService: RequestService,
|
||||
public entityTypeService: EntityTypeService,
|
||||
protected relationshipTypeService: RelationshipTypeService,
|
||||
public cdr: ChangeDetectorRef,
|
||||
protected modalService: NgbModal,
|
||||
) {
|
||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||
}
|
||||
@@ -77,27 +77,16 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||
|
||||
const label = this.item.firstMetadataValue('dspace.entity.type');
|
||||
if (label !== undefined) {
|
||||
this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks())
|
||||
.pipe(
|
||||
map((relationshipTypes: PaginatedList<RelationshipType>) => relationshipTypes.page)
|
||||
);
|
||||
|
||||
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
||||
this.relationshipTypes$ = this.entityType$.pipe(
|
||||
switchMap((entityType) =>
|
||||
this.entityTypeService.getEntityTypeRelationships(
|
||||
entityType.id,
|
||||
true,
|
||||
true,
|
||||
followLink('leftType'),
|
||||
followLink('rightType'))
|
||||
.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((relationshipTypes) => relationshipTypes.page),
|
||||
)
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.entityType$ = observableOf(undefined);
|
||||
}
|
||||
@@ -157,6 +146,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||
this.initializeOriginalFields();
|
||||
this.cdr.detectChanges();
|
||||
this.displayNotifications(response);
|
||||
this.modalService.dismissAll();
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -233,4 +223,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
getRelationshipTypeFollowLinks() {
|
||||
return [
|
||||
followLink('leftType'),
|
||||
followLink('rightType')
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
<div class="mt-4">
|
||||
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
||||
</div>
|
||||
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"
|
||||
[displayActions]="true"></ds-item-versions>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user