Merge branch 'DS-4515_submit-external-source' of github.com:4Science/dspace-angular into DS-4515_submit-external-source

This commit is contained in:
Giuseppe Digilio
2020-06-24 10:32:06 +02:00
16 changed files with 234 additions and 94 deletions

View File

@@ -22,6 +22,7 @@ import { NotificationsService } from '../../notifications/notifications.service'
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { VarDirective } from '../../utils/var.directive';
import { ComColFormComponent } from './comcol-form.component';
import { Operation } from 'fast-json-patch';
describe('ComColFormComponent', () => {
let comp: ComColFormComponent<DSpaceObject>;
@@ -40,11 +41,8 @@ describe('ComColFormComponent', () => {
}
};
const dcTitle = 'dc.title';
const dcRandom = 'dc.random';
const dcAbstract = 'dc.description.abstract';
const titleMD = { [dcTitle]: [{ value: 'Community Title', language: null }] };
const randomMD = { [dcRandom]: [{ value: 'Random metadata excluded from form', language: null }] };
const abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] };
const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] };
const formModel = [
@@ -112,33 +110,47 @@ describe('ComColFormComponent', () => {
});
it('should emit the new version of the community', () => {
comp.dso = Object.assign(
new Community(),
{
metadata: {
...titleMD,
...randomMD
}
}
);
comp.dso = new Community();
comp.onSubmit();
const operations: Operation[] = [
{
op: 'replace',
path: '/metadata/dc.title',
value: {
value: 'New Community Title',
language: null,
},
},
{
op: 'replace',
path: '/metadata/dc.description.abstract',
value: {
value: 'Community description',
language: null,
},
},
];
expect(comp.submitForm.emit).toHaveBeenCalledWith(
{
dso: Object.assign(
{},
new Community(),
{
dso: Object.assign({}, comp.dso, {
metadata: {
...newTitleMD,
...randomMD,
...abstractMD
'dc.title': [{
value: 'New Community Title',
language: null,
}],
'dc.description.abstract': [{
value: 'Community description',
language: null,
}],
},
type: Community.type
},
type: Community.type,
}
),
uploader: undefined,
deleteLogo: false
deleteLogo: false,
operations: operations,
}
);
})
@@ -164,11 +176,6 @@ describe('ComColFormComponent', () => {
it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled();
});
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
});
describe('onUploadError', () => {
@@ -239,6 +246,11 @@ describe('ComColFormComponent', () => {
it('should display a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
});
describe('when dsoService.deleteLogo returns an error response', () => {

View File

@@ -25,6 +25,7 @@ import { hasValue, isNotEmpty } from '../../empty.util';
import { NotificationsService } from '../../notifications/notifications.service';
import { UploaderOptions } from '../../uploader/uploader-options.model';
import { UploaderComponent } from '../../uploader/uploader.component';
import { Operation } from 'fast-json-patch';
/**
* A form for creating and editing Communities or Collections
@@ -85,7 +86,8 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
@Output() submitForm: EventEmitter<{
dso: T,
uploader: FileUploader,
deleteLogo: boolean
deleteLogo: boolean,
operations: Operation[],
}> = new EventEmitter();
/**
@@ -189,9 +191,9 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
const formMetadata = {} as MetadataMap;
this.formModel.forEach((fieldModel: DynamicInputModel) => {
const value: MetadataValue = {
value: fieldModel.value as string,
language: null
} as any;
value: fieldModel.value as string,
language: null
} as any;
if (formMetadata.hasOwnProperty(fieldModel.name)) {
formMetadata[fieldModel.name].push(value);
} else {
@@ -206,10 +208,26 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
},
type: Community.type
});
const operations: Operation[] = [];
this.formModel.forEach((fieldModel: DynamicInputModel) => {
if (fieldModel.value !== this.dso.firstMetadataValue(fieldModel.name)) {
operations.push({
op: 'replace',
path: `/metadata/${fieldModel.name}`,
value: {
value: fieldModel.value,
language: null,
},
});
}
});
this.submitForm.emit({
dso: updatedDSO,
uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined,
deleteLogo: this.markLogoForDeletion
deleteLogo: this.markLogoForDeletion,
operations: operations,
});
}
@@ -257,7 +275,9 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDe
* The request was successful, display a success notification
*/
public onCompleteItem() {
this.refreshCache();
if (hasValue(this.dso.id)) {
this.refreshCache();
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success'));
this.finish.emit();
}

View File

@@ -77,7 +77,8 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements
const uploader = event.uploader;
this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => {
this.dsoDataService.create(dso, new RequestParam('parent', uuid))
const params = uuid ? [new RequestParam('parent', uuid)] : [];
this.dsoDataService.create(dso, ...params)
.pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) {

View File

@@ -13,13 +13,13 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NotificationsService } from '../../../notifications/notifications.service';
import { SharedModule } from '../../../shared.module';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
import { ComcolMetadataComponent } from './comcol-metadata.component';
describe('ComColMetadataComponent', () => {
let comp: ComcolMetadataComponent<DSpaceObject>;
let fixture: ComponentFixture<ComcolMetadataComponent<DSpaceObject>>;
let dsoDataService: CommunityDataService;
let dsoDataService;
let router: Router;
let community;
@@ -49,6 +49,7 @@ describe('ComColMetadataComponent', () => {
communityDataServiceStub = {
update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
patch: () => null,
getLogoEndpoint: () => observableOf(logoEndpoint)
};
@@ -95,37 +96,60 @@ describe('ComColMetadataComponent', () => {
describe('with an empty queue in the uploader', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
operations: [
{
op: 'replace',
path: '/metadata/dc.title',
value: {
value: 'test',
language: null,
},
},
],
dso: new Community(),
uploader: {
options: {
url: ''
},
queue: [],
/* tslint:disable:no-empty */
uploadAll: () => {}
uploadAll: () => {
}
/* tslint:enable:no-empty */
}
}
},
deleteLogo: false,
};
spyOn(router, 'navigate');
});
it('should navigate when successful', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
describe('when successful', () => {
beforeEach(() => {
spyOn(dsoDataService, 'patch').and.returnValue(observableOf({
isSuccessful: true,
}));
});
it('should navigate', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
});
});
it('should not navigate on failure', () => {
spyOn(router, 'navigate');
spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity));
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
describe('on failure', () => {
beforeEach(() => {
spyOn(dsoDataService, 'patch').and.returnValue(observableOf({
isSuccessful: false,
}));
});
it('should not navigate', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { ActivatedRoute, Router } from '@angular/router';
import { first, map, take } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue, isNotUndefined } from '../../../empty.util';
import { DataService } from '../../../../core/data/data.service';
import { hasValue, isEmpty } from '../../../empty.util';
import { ResourceType } from '../../../../core/shared/resource-type';
import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { NotificationsService } from '../../../notifications/notifications.service';
@@ -49,26 +48,33 @@ export class ComcolMetadataComponent<TDomain extends DSpaceObject> implements On
* @param event The event returned by the community/collection form. Contains the new dso and logo uploader
*/
onSubmit(event) {
const dso = event.dso;
const uploader = event.uploader;
const deleteLogo = event.deleteLogo;
this.dsoDataService.update(dso)
.pipe(getSucceededRemoteData())
.subscribe((dsoRD: RemoteData<TDomain>) => {
if (isNotUndefined(dsoRD)) {
const newUUID = dsoRD.payload.uuid;
if (hasValue(uploader) && uploader.queue.length > 0) {
this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => {
uploader.options.url = href;
uploader.uploadAll();
});
} else if (!deleteLogo) {
this.router.navigate([this.frontendURL + newUUID]);
}
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success'));
}
const newLogo = hasValue(uploader) && uploader.queue.length > 0;
if (newLogo) {
this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => {
uploader.options.url = href;
uploader.uploadAll();
});
}
if (!isEmpty(event.operations)) {
this.dsoDataService.patch(event.dso, event.operations)
.subscribe(async (response) => {
if (response.isSuccessful) {
if (!newLogo && !deleteLogo) {
await this.router.navigate([this.frontendURL + event.dso.uuid]);
}
this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`));
} else if (response.statusCode === 403) {
this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`));
} else {
this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`));
}
});
}
}
/**

View File

@@ -39,7 +39,15 @@ describe('CreateCollectionParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -14,6 +14,6 @@
</h3>
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -33,7 +33,15 @@ describe('CreateCommunityParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -32,7 +32,15 @@ describe('CreateItemParentSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: collectionRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -5,6 +5,6 @@
</button>
</div>
<div class="modal-body">
<ds-dso-selector [currentDSOId]="(dsoRD$ | async)?.payload.uuid ? 'search.resourceid:' + (dsoRD$ | async)?.payload.uuid : null" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid ? 'search.resourceid:' + dsoRD?.payload.uuid : null" [type]="selectorType" (onSelect)="selectObject($event)"></ds-dso-selector>
</div>
</div>

View File

@@ -41,8 +41,16 @@ describe('DSOSelectorModalWrapperComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } }
}
useValue: {
root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
}
},
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
@@ -63,11 +71,7 @@ describe('DSOSelectorModalWrapperComponent', () => {
});
it('should initially set the DSO to the activated route\'s item/collection/community', () => {
component.dsoRD$
.pipe(first())
.subscribe((a) => {
expect(a).toEqual(itemRD);
})
expect(component.dsoRD).toEqual(itemRD);
});
describe('selectObject', () => {

View File

@@ -1,11 +1,12 @@
import { Injectable, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { RemoteData } from '../../../core/data/remote-data';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { map } from 'rxjs/operators';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { hasValue, isNotEmpty } from '../../empty.util';
export enum SelectorActionType {
CREATE = 'create',
@@ -21,7 +22,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
/**
* The current page's DSO
*/
@Input() dsoRD$: Observable<RemoteData<DSpaceObject>>;
@Input() dsoRD: RemoteData<DSpaceObject>;
/**
* The type of the DSO that's being edited or created
@@ -45,10 +46,30 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
* Get de current page's DSO based on the selectorType
*/
ngOnInit(): void {
const typeString = this.selectorType.toString().toLowerCase();
this.dsoRD$ = this.route.root.firstChild.firstChild.data.pipe(map((data) => data[typeString]));
const matchingRoute = this.findRouteData(
(route: ActivatedRouteSnapshot) => hasValue(route.data.dso),
this.route.root.snapshot
);
if (hasValue(matchingRoute)) {
this.dsoRD = matchingRoute.data.dso;
}
}
findRouteData(predicate: (value: ActivatedRouteSnapshot, index?: number, obj?: ActivatedRouteSnapshot[]) => unknown, ...routes: ActivatedRouteSnapshot[]) {
const result = routes.find(predicate);
if (hasValue(result)) {
return result;
} else {
const nextLevelRoutes = routes
.map((route: ActivatedRouteSnapshot) => route.children)
.reduce((combined: ActivatedRouteSnapshot[], current: ActivatedRouteSnapshot[]) => [...combined, ...current]);
if (isNotEmpty(nextLevelRoutes)) {
return this.findRouteData(predicate, ...nextLevelRoutes)
} else {
return undefined;
}
}
}
/**
* Method called when an object has been selected
* @param dso The selected DSpaceObject

View File

@@ -33,7 +33,15 @@ describe('EditCollectionSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: collectionRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -33,7 +33,15 @@ describe('EditCommunitySelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: communityRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -33,7 +33,15 @@ describe('EditItemSelectorComponent', () => {
{ provide: NgbActiveModal, useValue: modalStub },
{
provide: ActivatedRoute,
useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } }
useValue: {
root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
},
},
{
provide: Router, useValue: router

View File

@@ -767,6 +767,10 @@
"community.edit.notifications.success": "Successfully edited the Community",
"community.edit.notifications.unauthorized": "You do not have privileges to make this change",
"community.edit.notifications.error": "An error occured while editing the Community",
"community.edit.return": "Return",