diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index 74ca9aae4e..f68cfb0685 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -5,19 +5,16 @@

{{'item.edit.move.description' | translate}}

- - - +
+
{{'dso-selector.placeholder' | translate: { type: 'collection' } }}
+
+ + +
+
+
@@ -33,16 +30,24 @@
- - +
+
+ + + +
+
diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index dd91c65e1e..d200891629 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -21,6 +21,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { RequestService } from '../../../core/data/request.service'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; describe('ItemMoveComponent', () => { let comp: ItemMoveComponent; @@ -47,18 +49,25 @@ describe('ItemMoveComponent', () => { name: 'Test collection 2' }); - const mockItemDataService = jasmine.createSpyObj({ - moveToCollection: createSuccessfulRemoteDataObject$(collection1) + let itemDataService; + + const mockItemDataServiceSuccess = jasmine.createSpyObj({ + moveToCollection: createSuccessfulRemoteDataObject$(collection1), + findById: createSuccessfulRemoteDataObject$(mockItem), }); const mockItemDataServiceFail = jasmine.createSpyObj({ - moveToCollection: createFailedRemoteDataObject$('Internal server error', 500) + moveToCollection: createFailedRemoteDataObject$('Internal server error', 500), + findById: createSuccessfulRemoteDataObject$(mockItem), }); const routeStub = { data: observableOf({ dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), { - id: 'item1' + id: 'item1', + owningCollection: createSuccessfulRemoteDataObject$(Object.assign(new Collection(), { + id: 'originalOwningCollection', + })) })) }) }; @@ -79,43 +88,40 @@ describe('ItemMoveComponent', () => { const notificationsServiceStub = new NotificationsServiceStub(); + const init = (mockItemDataService) => { + itemDataService = mockItemDataService; + + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], + declarations: [ItemMoveComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + { provide: Router, useValue: routerStub }, + { provide: ItemDataService, useValue: mockItemDataService }, + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: SearchService, useValue: mockSearchService }, + { provide: RequestService, useValue: getMockRequestService() }, + ], schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }).compileComponents(); + fixture = TestBed.createComponent(ItemMoveComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }; + describe('ItemMoveComponent success', () => { beforeEach(() => { - TestBed.configureTestingModule({ - imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [ItemMoveComponent], - providers: [ - { provide: ActivatedRoute, useValue: routeStub }, - { provide: Router, useValue: routerStub }, - { provide: ItemDataService, useValue: mockItemDataService }, - { provide: NotificationsService, useValue: notificationsServiceStub }, - { provide: SearchService, useValue: mockSearchService }, - ], schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] - }).compileComponents(); - fixture = TestBed.createComponent(ItemMoveComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); + init(mockItemDataServiceSuccess); }); - it('should load suggestions', () => { - const expected = [ - collection1, - collection2 - ]; - comp.collectionSearchResults.subscribe((value) => { - expect(value).toEqual(expected); - } - ); - }); it('should get current url ', () => { expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit'); }); - it('should on click select the correct collection name and id', () => { + it('should select the correct collection name and id on click', () => { const data = collection1; - comp.onClick(data); + comp.selectDso(data); expect(comp.selectedCollectionName).toEqual('Test collection 1'); expect(comp.selectedCollection).toEqual(collection1); @@ -128,12 +134,12 @@ describe('ItemMoveComponent', () => { }); comp.selectedCollectionName = 'selected-collection-id'; comp.selectedCollection = collection1; - comp.moveCollection(); + comp.moveToCollection(); - expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1); + expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1); }); it('should call notificationsService success message on success', () => { - comp.moveCollection(); + comp.moveToCollection(); expect(notificationsServiceStub.success).toHaveBeenCalled(); }); @@ -142,26 +148,11 @@ describe('ItemMoveComponent', () => { describe('ItemMoveComponent fail', () => { beforeEach(() => { - TestBed.configureTestingModule({ - imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [ItemMoveComponent], - providers: [ - { provide: ActivatedRoute, useValue: routeStub }, - { provide: Router, useValue: routerStub }, - { provide: ItemDataService, useValue: mockItemDataServiceFail }, - { provide: NotificationsService, useValue: notificationsServiceStub }, - { provide: SearchService, useValue: mockSearchService }, - ], schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] - }).compileComponents(); - fixture = TestBed.createComponent(ItemMoveComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); + init(mockItemDataServiceFail); }); it('should call notificationsService error message on fail', () => { - comp.moveCollection(); + comp.moveToCollection(); expect(notificationsServiceStub.error).toHaveBeenCalled(); }); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index b1ed121b40..b7ab761fe5 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -1,25 +1,21 @@ import { Component, OnInit } from '@angular/core'; -import { first, map } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { RemoteData } from '../../../core/data/remote-data'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { - getFirstSucceededRemoteData, - getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload + getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload, } from '../../../core/shared/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs'; import { Collection } from '../../../core/shared/collection.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SearchService } from '../../../core/shared/search/search.service'; -import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; -import { SearchResult } from '../../../shared/search/search-result.model'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { RequestService } from '../../../core/data/request.service'; @Component({ selector: 'ds-item-move', @@ -38,7 +34,8 @@ export class ItemMoveComponent implements OnInit { inheritPolicies = false; itemRD$: Observable>; - collectionSearchResults: Observable = observableOf([]); + originalCollection: Collection; + selectedCollectionName: string; selectedCollection: Collection; canSubmit = false; @@ -46,23 +43,26 @@ export class ItemMoveComponent implements OnInit { item: Item; processing = false; - pagination = new PaginationComponentOptions(); - /** * Route to the item's page */ itemPageRoute$: Observable; + COLLECTIONS = [DSpaceObjectType.COLLECTION]; + constructor(private route: ActivatedRoute, private router: Router, private notificationsService: NotificationsService, private itemDataService: ItemDataService, private searchService: SearchService, - private translateService: TranslateService) { - } + private translateService: TranslateService, + private requestService: RequestService, + ) {} ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe( + map((data) => data.dso), getFirstSucceededRemoteData() + ) as Observable>; this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), map((item) => getItemPageRoute(item)) @@ -71,43 +71,22 @@ export class ItemMoveComponent implements OnInit { this.item = rd.payload; } ); - this.pagination.pageSize = 5; - this.loadSuggestions(''); - } - - /** - * Find suggestions based on entered query - * @param query - Search query - */ - findSuggestions(query): void { - this.loadSuggestions(query); - } - - /** - * Load all available collections to move the item to. - * TODO: When the API support it, only fetch collections where user has ADD rights to. - */ - loadSuggestions(query): void { - this.collectionSearchResults = this.searchService.search(new PaginatedSearchOptions({ - pagination: this.pagination, - dsoTypes: [DSpaceObjectType.COLLECTION], - query: query - })).pipe( - first(), - map((rd: RemoteData>>) => { - return rd.payload.page.map((searchResult) => { - return searchResult.indexableObject; - }); - }) , - ); - + this.itemRD$.pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + switchMap((item) => item.owningCollection), + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + ).subscribe((collection) => { + this.originalCollection = collection; + }); } /** * Set the collection name and id based on the selected value * @param data - obtained from the ds-input-suggestions component */ - onClick(data: any): void { + selectDso(data: any): void { this.selectedCollection = data; this.selectedCollectionName = data.name; this.canSubmit = true; @@ -123,26 +102,41 @@ export class ItemMoveComponent implements OnInit { /** * Moves the item to a new collection based on the selected collection */ - moveCollection() { + moveToCollection() { this.processing = true; - this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe( - (response: RemoteData) => { - this.router.navigate([getItemEditRoute(this.item)]); - if (response.hasSucceeded) { - this.notificationsService.success(this.translateService.get('item.edit.move.success')); - } else { - this.notificationsService.error(this.translateService.get('item.edit.move.error')); - } - this.processing = false; + const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection) + .pipe(getFirstCompletedRemoteData()); + + move$.subscribe((response: RemoteData) => { + if (response.hasSucceeded) { + this.notificationsService.success(this.translateService.get('item.edit.move.success')); + } else { + this.notificationsService.error(this.translateService.get('item.edit.move.error')); } - ); + }); + + move$.pipe( + switchMap(() => this.requestService.setStaleByHrefSubstring(this.item.id)), + switchMap(() => + this.itemDataService.findById( + this.item.id, + false, + true, + followLink('owningCollection') + )), + getFirstCompletedRemoteData() + ).subscribe(() => { + this.processing = false; + this.router.navigate([getItemEditRoute(this.item)]); + }); } - /** - * Resets the can submit when the user changes the content of the input field - * @param data - */ - resetCollection(data: any) { + discard(): void { + this.selectedCollection = null; this.canSubmit = false; } + + get canMove(): boolean { + return this.canSubmit && this.selectedCollection?.id !== this.originalCollection.id; + } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index e56f9f2b0c..7a0116fe86 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -23,14 +23,7 @@ import { DataService } from './data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { - DeleteRequest, - FindListOptions, - GetRequest, - PostRequest, - PutRequest, - RestRequest -} from './request.models'; +import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { Bundle } from '../shared/bundle.model'; @@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models'; import { BundleDataService } from './bundle-data.service'; import { Operation } from 'fast-json-patch'; import { NoContent } from '../shared/NoContent.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { ResponseParsingService } from './parsing.service'; +import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; @Injectable() @dataService(ITEM) @@ -229,7 +225,7 @@ export class ItemDataService extends DataService { * @param itemId * @param collection */ - public moveToCollection(itemId: string, collection: Collection): Observable> { + public moveToCollection(itemId: string, collection: Collection): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); @@ -242,9 +238,17 @@ export class ItemDataService extends DataService { find((href: string) => hasValue(href)), map((href: string) => { const request = new PutRequest(requestId, href, collection._links.self.href, options); - this.requestService.send(request); + Object.assign(request, { + // TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code + getResponseParser(): GenericConstructor { + return StatusCodeOnlyResponseParsingService; + } + }); + return request; }) - ).subscribe(); + ).subscribe((request) => { + this.requestService.send(request); + }); return this.rdbService.buildFromRequestUUID(requestId); } diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 122f37b031..ab2ea6cd8b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -14,12 +14,12 @@ [infiniteScrollContainer]="'.scrollable-menu'" [fromRoot]="true" (scrolled)="onScrollDown()"> - + -