diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 50f67c6cb8..bbe77f3e2f 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -238,6 +238,17 @@ "item.edit.modify.overview.field": "Field", "item.edit.modify.overview.language": "Language", "item.edit.modify.overview.value": "Value", + "item.edit.move.cancel": "Cancel", + "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "item.edit.move.error": "An error occured when attempting to move the item", + "item.edit.move.head": "Move item: {{id}}", + "item.edit.move.inheritpolicies.checkbox": "Inherit policies", + "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", + "item.edit.move.move": "Move", + "item.edit.move.processing": "Moving...", + "item.edit.move.search.placeholder": "Enter a search query to look for collections", + "item.edit.move.success": "The item has been moved succesfully", + "item.edit.move.title": "Move item", "item.edit.private.cancel": "Cancel", "item.edit.private.confirm": "Make it Private", "item.edit.private.description": "Are you sure this item should be made private in the archive?", diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 1542d12ce5..236388109e 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -18,6 +18,7 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component'; import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component'; +import { ItemMoveComponent } from './item-move/item-move.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -44,7 +45,8 @@ import { EditRelationshipListComponent } from './item-relationships/edit-relatio ItemBitstreamsComponent, EditInPlaceFieldComponent, EditRelationshipComponent, - EditRelationshipListComponent + EditRelationshipListComponent, + ItemMoveComponent, ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index f298f7df39..65e2a36fd1 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -10,6 +10,7 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; +import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; @@ -17,6 +18,7 @@ const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; const ITEM_EDIT_PRIVATE_PATH = 'private'; const ITEM_EDIT_PUBLIC_PATH = 'public'; const ITEM_EDIT_DELETE_PATH = 'delete'; +const ITEM_EDIT_MOVE_PATH = 'move'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -104,6 +106,14 @@ const ITEM_EDIT_DELETE_PATH = 'delete'; resolve: { item: ItemPageResolver } + }, + { + path: ITEM_EDIT_MOVE_PATH, + component: ItemMoveComponent, + data: { title: 'item.edit.move.title' }, + resolve: { + item: ItemPageResolver + } }]) ], providers: [ diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index e9c5de95ca..e8ffc28920 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -4,7 +4,7 @@ {{metadata?.key?.split('.').join('.​')}}
- + >
{{"item.edit.metadata.metadatafield.invalid" | translate}} diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index b8d122d4f6..e07df15651 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { SharedModule } from '../../../../shared/shared.module'; import { getTestScheduler } from 'jasmine-marbles'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { TestScheduler } from 'rxjs/testing'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { TranslateModule } from '@ngx-translate/core'; @@ -18,6 +17,7 @@ import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; let comp: EditInPlaceFieldComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 1722cde8bc..7dce025a73 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -4,13 +4,13 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { NgModel } from '@angular/forms'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; +import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; @Component({ // tslint:disable-next-line:component-selector 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 new file mode 100644 index 0000000000..cf5ada77cf --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -0,0 +1,48 @@ +
+
+
+

{{'item.edit.move.head' | translate: {id: (itemRD$ | async)?.payload?.handle} }}

+

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

+
+
+ + + +
+
+
+
+

+ + +

+

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

+
+
+ + + +
+
+
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 new file mode 100644 index 0000000000..e73b4b6f9a --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -0,0 +1,172 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemMoveComponent } from './item-move.component'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { SearchService } from '../../../+search-page/search-service/search.service'; +import { of as observableOf } from 'rxjs'; +import { FormsModule } from '@angular/forms'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { RestResponse } from '../../../core/cache/response.models'; +import { Collection } from '../../../core/shared/collection.model'; + +describe('ItemMoveComponent', () => { + let comp: ItemMoveComponent; + let fixture: ComponentFixture; + + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + handle: 'fake/handle', + lastModified: '2018' + }); + + const itemPageUrl = `fake-url/${mockItem.id}`; + const routerStub = Object.assign(new RouterStub(), { + url: `${itemPageUrl}/edit` + }); + + const mockItemDataService = jasmine.createSpyObj({ + moveToCollection: observableOf(new RestResponse(true, 200, 'Success')) + }); + + const mockItemDataServiceFail = jasmine.createSpyObj({ + moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error')) + }); + + const routeStub = { + data: observableOf({ + item: new RemoteData(false, false, true, null, { + id: 'item1' + }) + }) + }; + + const collection1 = Object.assign(new Collection(),{ + uuid: 'collection-uuid-1', + name: 'Test collection 1', + self: 'self-link-1', + }); + + const collection2 = Object.assign(new Collection(),{ + uuid: 'collection-uuid-2', + name: 'Test collection 2', + self: 'self-link-2', + }); + + const mockSearchService = { + search: () => { + return observableOf(new RemoteData(false, false, true, null, + new PaginatedList(null, [ + { + indexableObject: collection1, + hitHighlights: {} + }, { + indexableObject: collection2, + hitHighlights: {} + } + ]))); + } + }; + + const notificationsServiceStub = new NotificationsServiceStub(); + + describe('ItemMoveComponent success', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + 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(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemMoveComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + 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', () => { + const data = collection1; + + comp.onClick(data); + + expect(comp.selectedCollectionName).toEqual('Test collection 1'); + expect(comp.selectedCollection).toEqual(collection1); + }); + describe('moveCollection', () => { + it('should call itemDataService.moveToCollection', () => { + comp.itemId = 'item-id'; + comp.selectedCollectionName = 'selected-collection-id'; + comp.selectedCollection = collection1; + comp.moveCollection(); + + expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1); + }); + it('should call notificationsService success message on success', () => { + comp.moveCollection(); + + expect(notificationsServiceStub.success).toHaveBeenCalled(); + }); + }); + }); + + describe('ItemMoveComponent fail', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + 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(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemMoveComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should call notificationsService error message on fail', () => { + comp.moveCollection(); + + 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 new file mode 100644 index 0000000000..113ee97b3f --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { SearchService } from '../../../+search-page/search-service/search.service'; +import { first, map } from 'rxjs/operators'; +import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { SearchOptions } from '../../../+search-page/search-options.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { SearchResult } from '../../../+search-page/search-result.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 { getSucceededRemoteData } from '../../../core/shared/operators'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { getItemEditPath } from '../../item-page-routing.module'; +import { Observable, of as observableOf } from 'rxjs'; +import { RestResponse } from '../../../core/cache/response.models'; +import { Collection } from '../../../core/shared/collection.model'; +import { tap } from 'rxjs/internal/operators/tap'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; + +@Component({ + selector: 'ds-item-move', + templateUrl: './item-move.component.html' +}) +/** + * Component that handles the moving of an item to a different collection + */ +export class ItemMoveComponent implements OnInit { + /** + * TODO: There is currently no backend support to change the owningCollection and inherit policies, + * TODO: when this is added, the inherit policies option should be used. + */ + + selectorType = DSpaceObjectType.COLLECTION; + + inheritPolicies = false; + itemRD$: Observable>; + collectionSearchResults: Observable = observableOf([]); + selectedCollectionName: string; + selectedCollection: Collection; + canSubmit = false; + + itemId: string; + processing = false; + + pagination = new PaginationComponentOptions(); + + constructor(private route: ActivatedRoute, + private router: Router, + private notificationsService: NotificationsService, + private itemDataService: ItemDataService, + private searchService: SearchService, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.pipe(map((data) => data.item), getSucceededRemoteData()) as Observable>; + this.itemRD$.subscribe((rd) => { + this.itemId = rd.payload.id; + } + ); + 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, + dsoType: DSpaceObjectType.COLLECTION, + query: query + })).pipe( + first(), + map((rd: RemoteData>>) => { + return rd.payload.page.map((searchResult) => { + return searchResult.indexableObject + }) + }) , + ); + + } + + /** + * Set the collection name and id based on the selected value + * @param data - obtained from the ds-input-suggestions component + */ + onClick(data: any): void { + this.selectedCollection = data; + this.selectedCollectionName = data.name; + this.canSubmit = true; + } + + /** + * @returns {string} the current URL + */ + getCurrentUrl() { + return this.router.url; + } + + /** + * Moves the item to a new collection based on the selected collection + */ + moveCollection() { + this.processing = true; + this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(first()).subscribe( + (response: RestResponse) => { + this.router.navigate([getItemEditPath(this.itemId)]); + if (response.isSuccessful) { + this.notificationsService.success(this.translateService.get('item.edit.move.success')); + } else { + this.notificationsService.error(this.translateService.get('item.edit.move.error')); + } + this.processing = false; + } + ); + } + + /** + * Resets the can submit when the user changes the content of the input field + * @param data + */ + resetCollection(data: any) { + this.canSubmit = false; + } +} diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html index 4623195437..3a52fd0d12 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html @@ -4,7 +4,7 @@ diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 1901bf5fb4..7122dbaf42 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -1,8 +1,9 @@ -import {ItemOperation} from './itemOperation.model'; -import {async, TestBed} from '@angular/core/testing'; -import {ItemOperationComponent} from './item-operation.component'; -import {TranslateModule} from '@ngx-translate/core'; -import {By} from '@angular/platform-browser'; +import { ItemOperation } from './itemOperation.model'; +import { async, TestBed } from '@angular/core/testing'; +import { ItemOperationComponent } from './item-operation.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; describe('ItemOperationComponent', () => { let itemOperation: ItemOperation; @@ -12,7 +13,7 @@ describe('ItemOperationComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], declarations: [ItemOperationComponent] }).compileComponents(); })); diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index c7e3a023d1..d293188aa6 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -79,6 +79,7 @@ export class ItemStatusComponent implements OnInit { this.operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public')); } this.operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete')); + this.operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move')); }); } diff --git a/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index 63d034c6ea..5e6bcfaf8b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -15,7 +15,7 @@ | translate}} - + ngDefaultControl> diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index ccbda54f89..cb25aba44e 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -21,9 +21,9 @@ import { SearchService } from '../../../search-service/search.service'; import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service'; import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { SearchOptions } from '../../../search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component'; +import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; @Component({ selector: 'ds-search-facet-filter', diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 996fd7f751..06b60b5ecd 100644 --- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -15,7 +15,7 @@ | translate}} - + > diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html index db6a566758..43134014e1 100644 --- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html @@ -15,7 +15,7 @@ | translate}} - + ngDefaultControl> diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f6adbb23c2..07d8ed8405 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,8 +1,8 @@ -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -12,14 +12,17 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindAllOptions, PatchRequest, RestRequest } from './request.models'; +import { FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { configureRequest, getRequestFromRequestHref } from '../shared/operators'; import { RequestEntry } from './request.reducer'; +import { RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Collection } from '../shared/collection.model'; @Injectable() export class ItemDataService extends DataService { @@ -118,4 +121,43 @@ export class ItemDataService extends DataService { map((requestEntry: RequestEntry) => requestEntry.response) ); } + + /** + * Get the endpoint to move the item + * @param itemId + */ + public getMoveItemEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, itemId)), + map((endpoint: string) => `${endpoint}/owningCollection`) + ); + } + + /** + * Move the item to a different owning collection + * @param itemId + * @param collection + */ + public moveToCollection(itemId: string, collection: Collection): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getMoveItemEndpoint(itemId); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PutRequest(requestId, href, collection.self, options); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } } diff --git a/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html new file mode 100644 index 0000000000..2b605ccdc6 --- /dev/null +++ b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html @@ -0,0 +1,25 @@ +
+ + + +
+ diff --git a/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts new file mode 100644 index 0000000000..4229060e86 --- /dev/null +++ b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DsoInputSuggestionsComponent } from './dso-input-suggestions.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +describe('DsoInputSuggestionsComponent', () => { + + let comp: DsoInputSuggestionsComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + + const dso1 = { + uuid: 'test-uuid-1', + name: 'test-name-1' + } as DSpaceObject; + + const dso2 = { + uuid: 'test-uuid-2', + name: 'test-name-2' + } as DSpaceObject; + + const dso3 = { + uuid: 'test-uuid-3', + name: 'test-name-3' + } as DSpaceObject; + + const suggestions = [dso1, dso2, dso3]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], + declarations: [DsoInputSuggestionsComponent], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(DsoInputSuggestionsComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoInputSuggestionsComponent); + + comp = fixture.componentInstance; // LoadingComponent test instance + comp.suggestions = suggestions; + // query for the message