From 6b986c8c9172549b8a90c84401213c76b33d1c74 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 09:44:01 +0200 Subject: [PATCH 01/78] 55693: Item mapper page + ItemSelectComponent --- resources/i18n/en.json | 13 ++++ .../collection-item-mapper.component.html | 45 ++++++++++++ .../collection-item-mapper.component.scss | 5 ++ .../collection-item-mapper.component.spec.ts | 0 .../collection-item-mapper.component.ts | 69 +++++++++++++++++++ .../collection-page-routing.module.ts | 9 +++ .../collection-page.module.ts | 2 + .../item-select/item-select.component.html | 20 ++++++ .../item-select/item-select.component.scss | 0 .../item-select/item-select.component.spec.ts | 0 .../item-select/item-select.component.ts | 28 ++++++++ src/app/shared/shared.module.ts | 4 +- 12 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html create mode 100644 src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss create mode 100644 src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts create mode 100644 src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts create mode 100644 src/app/shared/item-select/item-select.component.html create mode 100644 src/app/shared/item-select/item-select.component.scss create mode 100644 src/app/shared/item-select/item-select.component.spec.ts create mode 100644 src/app/shared/item-select/item-select.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index c7cb9a5ba7..e75441c477 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -13,6 +13,12 @@ "head": "Recent Submissions" } } + }, + "item-mapper": { + "head": "Item Mapper - Map Items from Other Collections", + "collection": "Collection: \"{{name}}\"", + "description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "return": "Return" } }, "community": { @@ -43,6 +49,13 @@ "simple": "Simple item page", "full": "Full item page" } + }, + "select": { + "table": { + "collection": "Collection", + "author": "Author", + "title": "Title" + } } }, "nav": { diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html new file mode 100644 index 0000000000..078eabd1c7 --- /dev/null +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -0,0 +1,45 @@ +
+
+
+

{{'collection.item-mapper.head' | translate}}

+

+

{{'collection.item-mapper.description' | translate}}

+ +
+
+ + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+ + +
+
+
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss new file mode 100644 index 0000000000..4414c21645 --- /dev/null +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss @@ -0,0 +1,5 @@ +@import '../../../styles/variables.scss'; + +.tab:hover { + cursor: pointer; +} diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts new file mode 100644 index 0000000000..4456d3138e --- /dev/null +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -0,0 +1,69 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { fadeIn, fadeInOut } from '../../shared/animations/fade'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { ActivatedRoute, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { Observable } from 'rxjs/Observable'; +import { Collection } from '../../core/shared/collection.model'; +import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; +import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { Item } from '../../core/shared/item.model'; +import { combineLatest, flatMap, map, tap } from 'rxjs/operators'; +import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; +import { SearchService } from '../../+search-page/search-service/search.service'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; + +@Component({ + selector: 'ds-collection-item-mapper', + styleUrls: ['./collection-item-mapper.component.scss'], + templateUrl: './collection-item-mapper.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeIn, + fadeInOut + ] +}) +export class CollectionItemMapperComponent implements OnInit { + + collectionRD$: Observable>; + searchOptions$: Observable; + collectionItemsRD$: Observable>>; + mappingItemsRD$: Observable>>; + + activeTab = 0; + + constructor(private collectionDataService: CollectionDataService, + private route: ActivatedRoute, + private router: Router, + private searchConfigService: SearchConfigurationService, + private searchService: SearchService) { + } + + ngOnInit(): void { + this.collectionRD$ = this.route.data.map((data) => data.collection); + this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; + this.collectionItemsRD$ = this.collectionRD$.pipe( + getSucceededRemoteData(), + combineLatest(this.searchOptions$), + flatMap(([collectionRD, options]) => { + return this.searchService.search(Object.assign(options, { + scope: collectionRD.payload.id + })); + }), + toDSpaceObjectListRD() + ); + this.mappingItemsRD$ = this.searchOptions$.pipe( + flatMap((options: PaginatedSearchOptions) => this.searchService.search(options)), + toDSpaceObjectListRD() + ); + } + + getCurrentUrl(): string { + const urlTree = this.router.parseUrl(this.router.url); + const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; + return '/' + g.toString(); + } + +} diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index ca56bca2cd..c85d102437 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule } from '@angular/router'; import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageResolver } from './collection-page.resolver'; +import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; @NgModule({ imports: [ @@ -14,6 +15,14 @@ import { CollectionPageResolver } from './collection-page.resolver'; resolve: { collection: CollectionPageResolver } + }, + { + path: ':id/mapper', + component: CollectionItemMapperComponent, + pathMatch: 'full', + resolve: { + collection: CollectionPageResolver + } } ]) ], diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index 85462e67a3..79efea46c0 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module'; import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageRoutingModule } from './collection-page-routing.module'; import { SearchPageModule } from '../+search-page/search-page.module'; +import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; @NgModule({ imports: [ @@ -16,6 +17,7 @@ import { SearchPageModule } from '../+search-page/search-page.module'; ], declarations: [ CollectionPageComponent, + CollectionItemMapperComponent ] }) export class CollectionPageModule { diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html new file mode 100644 index 0000000000..c2d02b4529 --- /dev/null +++ b/src/app/shared/item-select/item-select.component.html @@ -0,0 +1,20 @@ +
+ + + + + + + + + + + + + + + + + +
{{'item.select.table.collection' | translate}}{{'item.select.table.author' | translate}}{{'item.select.table.title' | translate}}
{{(item.owningCollection | async)?.payload?.name}}{{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}}{{item.findMetadata("dc.title")}}
+
diff --git a/src/app/shared/item-select/item-select.component.scss b/src/app/shared/item-select/item-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/item-select/item-select.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts new file mode 100644 index 0000000000..7159d94962 --- /dev/null +++ b/src/app/shared/item-select/item-select.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { RemoteData } from '../../core/data/remote-data'; +import { Observable } from 'rxjs/Observable'; +import { Item } from '../../core/shared/item.model'; + +@Component({ + selector: 'ds-item-select', + styleUrls: ['./item-select.component.scss'], + templateUrl: './item-select.component.html' +}) + +export class ItemSelectComponent implements OnInit { + + @Input() + items$: Observable>>; + + checked: boolean[] = []; + + constructor(private itemDataService: ItemDataService) { + } + + ngOnInit(): void { + this.items$ = this.itemDataService.findAll({}); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index b6122dc70a..c5c6cad09b 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -83,6 +83,7 @@ import { InputSuggestionsComponent } from './input-suggestions/input-suggestions import { CapitalizePipe } from './utils/capitalize.pipe'; import { MomentModule } from 'angular2-moment'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; +import { ItemSelectComponent } from './item-select/item-select.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -156,7 +157,8 @@ const COMPONENTS = [ TruncatableComponent, TruncatablePartComponent, BrowseByComponent, - InputSuggestionsComponent + InputSuggestionsComponent, + ItemSelectComponent ]; const ENTRY_COMPONENTS = [ From 315b6a9690fa7f3c7f0ed72a57c0ea2e37d60b22 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 11:02:11 +0200 Subject: [PATCH 02/78] 55693: Intermediate commit --- .../collection-item-mapper.component.html | 2 +- .../collection-item-mapper.component.ts | 9 +++- .../item-select/item-select.component.html | 47 +++++++++++-------- .../item-select/item-select.component.ts | 10 ++-- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 078eabd1c7..1f7dd29396 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -35,7 +35,7 @@
- +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 4456d3138e..81b964de36 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -49,13 +49,18 @@ export class CollectionItemMapperComponent implements OnInit { combineLatest(this.searchOptions$), flatMap(([collectionRD, options]) => { return this.searchService.search(Object.assign(options, { - scope: collectionRD.payload.id + scope: collectionRD.payload.id, + dsoType: DSpaceObjectType.ITEM })); }), toDSpaceObjectListRD() ); this.mappingItemsRD$ = this.searchOptions$.pipe( - flatMap((options: PaginatedSearchOptions) => this.searchService.search(options)), + flatMap((options: PaginatedSearchOptions) => { + return this.searchService.search(Object.assign(options, { + dsoType: DSpaceObjectType.ITEM + })); + }), toDSpaceObjectListRD() ); } diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index c2d02b4529..1705495a20 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -1,20 +1,27 @@ -
- - - - - - - - - - - - - - - - - -
{{'item.select.table.collection' | translate}}{{'item.select.table.author' | translate}}{{'item.select.table.title' | translate}}
{{(item.owningCollection | async)?.payload?.name}}{{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}}{{item.findMetadata("dc.title")}}
-
+ +
+ + + + + + + + + + + + + + + + + +
{{'item.select.table.collection' | translate}}{{'item.select.table.author' | translate}}{{'item.select.table.title' | translate}}
{{(item.owningCollection | async)?.payload?.name}}{{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}}{{item.findMetadata("dc.title")}}
+
+
diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index 7159d94962..e8ac0eb6f4 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -1,9 +1,10 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; import { RemoteData } from '../../core/data/remote-data'; import { Observable } from 'rxjs/Observable'; import { Item } from '../../core/shared/item.model'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; @Component({ selector: 'ds-item-select', @@ -14,7 +15,10 @@ import { Item } from '../../core/shared/item.model'; export class ItemSelectComponent implements OnInit { @Input() - items$: Observable>>; + itemsRD$: Observable>>; + + @Input() + paginationOptions: PaginationComponentOptions; checked: boolean[] = []; @@ -22,7 +26,7 @@ export class ItemSelectComponent implements OnInit { } ngOnInit(): void { - this.items$ = this.itemDataService.findAll({}); + this.itemsRD$.subscribe((value) => console.log(value)); } } From 3dc09062d043ac27b133a203dce1fe34820e40dc Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 11:45:10 +0200 Subject: [PATCH 03/78] 55693: Tabset --- resources/i18n/en.json | 4 ++ .../collection-item-mapper.component.html | 39 ++++++++----------- .../collection-item-mapper.component.scss | 4 -- .../collection-item-mapper.component.ts | 2 - 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index e75441c477..63ee598199 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -18,6 +18,10 @@ "head": "Item Mapper - Map Items from Other Collections", "collection": "Collection: \"{{name}}\"", "description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "tabs": { + "browse": "Browse", + "map": "Map" + }, "return": "Return" } }, diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 1f7dd29396..3fdbdb4e7e 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -14,30 +14,23 @@ - -
- -
- - -
- - -
- -
-
+ + + + + + + + + + + + + diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss index 4414c21645..50be6f5ad0 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.scss @@ -1,5 +1 @@ @import '../../../styles/variables.scss'; - -.tab:hover { - cursor: pointer; -} diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 81b964de36..af0735acf7 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -32,8 +32,6 @@ export class CollectionItemMapperComponent implements OnInit { collectionItemsRD$: Observable>>; mappingItemsRD$: Observable>>; - activeTab = 0; - constructor(private collectionDataService: CollectionDataService, private route: ActivatedRoute, private router: Router, From 9440401ca34e49a7cfa89aed69b221bae760a56f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 13:16:56 +0200 Subject: [PATCH 04/78] 55693: Fixed scope being passed between tabs --- .../collection-item-mapper.component.html | 16 ++++++++++------ .../collection-item-mapper.component.ts | 11 +++++++++-- .../item-select/item-select.component.html | 5 +++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 3fdbdb4e7e..f8d770423b 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -18,16 +18,20 @@ - - +
+ + +
- +
+ +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index af0735acf7..622bd14547 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -14,6 +14,7 @@ import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/ import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; @Component({ selector: 'ds-collection-item-mapper', @@ -32,6 +33,8 @@ export class CollectionItemMapperComponent implements OnInit { collectionItemsRD$: Observable>>; mappingItemsRD$: Observable>>; + defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); + constructor(private collectionDataService: CollectionDataService, private route: ActivatedRoute, private router: Router, @@ -48,15 +51,19 @@ export class CollectionItemMapperComponent implements OnInit { flatMap(([collectionRD, options]) => { return this.searchService.search(Object.assign(options, { scope: collectionRD.payload.id, - dsoType: DSpaceObjectType.ITEM + dsoType: DSpaceObjectType.ITEM, + sort: this.defaultSortOptions })); }), toDSpaceObjectListRD() ); this.mappingItemsRD$ = this.searchOptions$.pipe( flatMap((options: PaginatedSearchOptions) => { + options.sort.field = 'dc.title'; return this.searchService.search(Object.assign(options, { - dsoType: DSpaceObjectType.ITEM + scope: undefined, + dsoType: DSpaceObjectType.ITEM, + sort: this.defaultSortOptions })); }), toDSpaceObjectListRD() diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index 1705495a20..d92d87e156 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -3,7 +3,8 @@ [paginationOptions]="paginationOptions" [pageInfoState]="(itemsRD$ | async)?.payload" [collectionSize]="(itemsRD$ | async)?.payload?.totalElements" - [hidePagerWhenSinglePage]="true"> + [hidePagerWhenSinglePage]="true" + [hideGear]="true">
@@ -18,7 +19,7 @@ - + From 5040d230fb46568bceb5ca85a12ae7f3dce7d9eb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 17:30:24 +0200 Subject: [PATCH 05/78] 55693: (incomplete) store interraction for selecting items --- src/app/app.reducer.ts | 7 +- src/app/core/core.module.ts | 2 + .../shared/item-select/item-select.actions.ts | 75 +++++++++++++ .../item-select/item-select.component.html | 4 +- .../item-select/item-select.component.ts | 11 +- .../shared/item-select/item-select.reducer.ts | 84 +++++++++++++++ .../shared/item-select/item-select.service.ts | 101 ++++++++++++++++++ 7 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 src/app/shared/item-select/item-select.actions.ts create mode 100644 src/app/shared/item-select/item-select.reducer.ts create mode 100644 src/app/shared/item-select/item-select.service.ts diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 8dc82dfb6f..ba882b50b8 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -14,6 +14,7 @@ import { } from './+search-page/search-filters/search-filter/search-filter.reducer'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; +import { itemSelectionReducer, ItemSelectionsState } from './shared/item-select/item-select.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -23,7 +24,8 @@ export interface AppState { notifications: NotificationsState; searchSidebar: SearchSidebarState; searchFilter: SearchFiltersState; - truncatable: TruncatablesState; + truncatable: TruncatablesState, + itemSelection: ItemSelectionsState } export const appReducers: ActionReducerMap = { @@ -34,7 +36,8 @@ export const appReducers: ActionReducerMap = { notifications: notificationsReducer, searchSidebar: sidebarReducer, searchFilter: filterReducer, - truncatable: truncatableReducer + truncatable: truncatableReducer, + itemSelection: itemSelectionReducer }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 73e97c7933..31b9b31244 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -64,6 +64,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; +import { ItemSelectService } from '../shared/item-select/item-select.service'; const IMPORTS = [ CommonModule, @@ -128,6 +129,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, + ItemSelectService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/shared/item-select/item-select.actions.ts b/src/app/shared/item-select/item-select.actions.ts new file mode 100644 index 0000000000..0f17575a28 --- /dev/null +++ b/src/app/shared/item-select/item-select.actions.ts @@ -0,0 +1,75 @@ +import { type } from '../ngrx/type'; +import { Action } from '@ngrx/store'; + +export const ItemSelectionActionTypes = { + INITIAL_DESELECT: type('dspace/item-select/INITIAL_DESELECT'), + INITIAL_SELECT: type('dspace/item-select/INITIAL_SELECT'), + SELECT: type('dspace/item-select/SELECT'), + DESELECT: type('dspace/item-select/DESELECT'), + SWITCH: type('dspace/item-select/SWITCH'), + RESET: type('dspace/item-select/RESET') +}; + +export class ItemSelectionAction implements Action { + /** + * UUID of the item a select action can be performed on + */ + id: string; + + /** + * Type of action that will be performed + */ + type; + + /** + * Initialize with the item's UUID + * @param {string} id of the item + */ + constructor(id: string) { + this.id = id; + } +} + +/* tslint:disable:max-classes-per-file */ +/** + * Used to set the initial state to deselected + */ +export class ItemSelectionInitialDeselectAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.INITIAL_DESELECT; +} + +/** + * Used to set the initial state to selected + */ +export class ItemSelectionInitialSelectAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.INITIAL_SELECT; +} + +/** + * Used to select an item + */ +export class ItemSelectionSelectAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.SELECT; +} + +/** + * Used to deselect an item + */ +export class ItemSelectionDeselectAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.DESELECT; +} + +/** + * Used to switch an item between selected and deselected + */ +export class ItemSelectionSwitchAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.SWITCH; +} + +/** + * Used to reset all item's selected to be deselected + */ +export class ItemSelectionResetAction extends ItemSelectionAction { + type = ItemSelectionActionTypes.RESET; +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index d92d87e156..c20e2cd6c1 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -16,8 +16,8 @@ - - + + diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index e8ac0eb6f4..e24ce1c4fa 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -5,6 +5,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { Observable } from 'rxjs/Observable'; import { Item } from '../../core/shared/item.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { ItemSelectService } from './item-select.service'; @Component({ selector: 'ds-item-select', @@ -22,11 +23,19 @@ export class ItemSelectComponent implements OnInit { checked: boolean[] = []; - constructor(private itemDataService: ItemDataService) { + constructor(private itemSelectService: ItemSelectService) { } ngOnInit(): void { this.itemsRD$.subscribe((value) => console.log(value)); } + switch(id: string) { + this.itemSelectService.switch(id); + } + + getSelected(id: string): Observable { + return this.itemSelectService.getSelected(id); + } + } diff --git a/src/app/shared/item-select/item-select.reducer.ts b/src/app/shared/item-select/item-select.reducer.ts new file mode 100644 index 0000000000..1ea443850b --- /dev/null +++ b/src/app/shared/item-select/item-select.reducer.ts @@ -0,0 +1,84 @@ +import { isEmpty } from '../empty.util'; +import { ItemSelectionAction, ItemSelectionActionTypes } from './item-select.actions'; + +/** + * Interface that represents the state for a single filters + */ +export interface ItemSelectionState { + checked: boolean; +} + +/** + * Interface that represents the state for all available filters + */ +export interface ItemSelectionsState { + [id: string]: ItemSelectionState +} + +const initialState: ItemSelectionsState = Object.create(null); + +/** + * Performs a search filter action on the current state + * @param {SearchFiltersState} state The state before the action is performed + * @param {SearchFilterAction} action The action that should be performed + * @returns {SearchFiltersState} The state after the action is performed + */ +export function itemSelectionReducer(state = initialState, action: ItemSelectionAction): ItemSelectionsState { + + switch (action.type) { + + case ItemSelectionActionTypes.INITIAL_SELECT: { + if (isEmpty(state) || isEmpty(state[action.id])) { + return Object.assign({}, state, { + [action.id]: { + checked: true + } + }); + } + return state; + } + + case ItemSelectionActionTypes.INITIAL_DESELECT: { + if (isEmpty(state) || isEmpty(state[action.id])) { + return Object.assign({}, state, { + [action.id]: { + checked: false + } + }); + } + return state; + } + + case ItemSelectionActionTypes.SELECT: { + return Object.assign({}, state, { + [action.id]: { + checked: true + } + }); + } + + case ItemSelectionActionTypes.DESELECT: { + return Object.assign({}, state, { + [action.id]: { + checked: false + } + }); + } + + case ItemSelectionActionTypes.SWITCH: { + return Object.assign({}, state, { + [action.id]: { + checked: !state.checked + } + }); + } + + case ItemSelectionActionTypes.RESET: { + return {}; + } + + default: { + return state; + } + } +} diff --git a/src/app/shared/item-select/item-select.service.ts b/src/app/shared/item-select/item-select.service.ts new file mode 100644 index 0000000000..3ba9f9a579 --- /dev/null +++ b/src/app/shared/item-select/item-select.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@angular/core'; +import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { ItemSelectionsState, ItemSelectionState } from './item-select.reducer'; +import { + ItemSelectionDeselectAction, + ItemSelectionInitialDeselectAction, + ItemSelectionInitialSelectAction, ItemSelectionResetAction, + ItemSelectionSelectAction, ItemSelectionSwitchAction +} from './item-select.actions'; +import { Observable } from 'rxjs/Observable'; +import { hasValue } from '../empty.util'; + +const selectionStateSelector = (state: ItemSelectionsState) => state.selectionItem; + +/** + * Service that takes care of selecting and deselecting items + */ +@Injectable() +export class ItemSelectService { + + constructor(private store: Store) { + } + + /** + * Request the current selection of a given item + * @param {string} id The UUID of the item + * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false + */ + getSelected(id: string): Observable { + return this.store.select(selectionByIdSelector(id)) + .map((object: ItemSelectionState) => { + if (object) { + return object.checked; + } else { + return false; + } + }); + } + + /** + * Dispatches an initial select action to the store for a given item + * @param {string} id The UUID of the item to select + */ + public initialSelect(id: string): void { + this.store.dispatch(new ItemSelectionInitialSelectAction(id)); + } + + /** + * Dispatches an initial deselect action to the store for a given item + * @param {string} id The UUID of the item to deselect + */ + public initialDeselect(id: string): void { + this.store.dispatch(new ItemSelectionInitialDeselectAction(id)); + } + + /** + * Dispatches a select action to the store for a given item + * @param {string} id The UUID of the item to select + */ + public select(id: string): void { + this.store.dispatch(new ItemSelectionSelectAction(id)); + } + + /** + * Dispatches a deselect action to the store for a given item + * @param {string} id The UUID of the item to deselect + */ + public deselect(id: string): void { + this.store.dispatch(new ItemSelectionDeselectAction(id)); + } + + /** + * Dispatches a switch action to the store for a given item + * @param {string} id The UUID of the item to select + */ + public switch(id: string): void { + this.store.dispatch(new ItemSelectionSwitchAction(id)); + } + + /** + * Dispatches a reset action to the store for all items + */ + public reset(): void { + this.store.dispatch(new ItemSelectionResetAction(null)); + } + +} + +function selectionByIdSelector(id: string): MemoizedSelector { + return keySelector(id); +} + +export function keySelector(key: string): MemoizedSelector { + return createSelector(selectionStateSelector, (state: ItemSelectionState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} From 09a84edb090893e3717df93988f8f537a8176656 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Sep 2018 11:09:36 +0200 Subject: [PATCH 06/78] 55693: Working store interaction for selecting items --- resources/i18n/en.json | 3 +- .../collection-item-mapper.component.html | 2 +- .../collection-item-mapper.component.ts | 4 +++ .../item-select/item-select.component.html | 3 +- .../item-select/item-select.component.ts | 15 ++++++++-- .../shared/item-select/item-select.reducer.ts | 2 +- .../shared/item-select/item-select.service.ts | 28 +++++++++++++++---- 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 63ee598199..948f538e9a 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -59,7 +59,8 @@ "collection": "Collection", "author": "Author", "title": "Title" - } + }, + "confirm": "Confirm selected" } }, "nav": { diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index f8d770423b..d201ce0ad6 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -30,7 +30,7 @@
- +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 622bd14547..f796abc54a 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -70,6 +70,10 @@ export class CollectionItemMapperComponent implements OnInit { ); } + mapItems(ids: string[]) { + console.log(ids); + } + getCurrentUrl(): string { const urlTree = this.router.parseUrl(this.router.url); const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index c20e2cd6c1..4047884bc4 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -17,7 +17,7 @@
- + @@ -26,3 +26,4 @@
{{(item.owningCollection | async)?.payload?.name}}{{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}}{{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}}
{{(item.owningCollection | async)?.payload?.name}} {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}}
{{(item.owningCollection | async)?.payload?.name}} {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}}
+ diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index e24ce1c4fa..93128aa1d5 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; import { RemoteData } from '../../core/data/remote-data'; @@ -21,13 +21,16 @@ export class ItemSelectComponent implements OnInit { @Input() paginationOptions: PaginationComponentOptions; - checked: boolean[] = []; + @Output() + confirm: EventEmitter = new EventEmitter(); + + selectedIds$: Observable; constructor(private itemSelectService: ItemSelectService) { } ngOnInit(): void { - this.itemsRD$.subscribe((value) => console.log(value)); + this.selectedIds$ = this.itemSelectService.getAllSelected(); } switch(id: string) { @@ -38,4 +41,10 @@ export class ItemSelectComponent implements OnInit { return this.itemSelectService.getSelected(id); } + confirmSelected() { + this.selectedIds$.subscribe((ids: string[]) => { + this.confirm.emit(ids); + }); + } + } diff --git a/src/app/shared/item-select/item-select.reducer.ts b/src/app/shared/item-select/item-select.reducer.ts index 1ea443850b..6306adf0c4 100644 --- a/src/app/shared/item-select/item-select.reducer.ts +++ b/src/app/shared/item-select/item-select.reducer.ts @@ -68,7 +68,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection case ItemSelectionActionTypes.SWITCH: { return Object.assign({}, state, { [action.id]: { - checked: !state.checked + checked: (isEmpty(state) || isEmpty(state[action.id])) ? true : !state[action.id].checked } }); } diff --git a/src/app/shared/item-select/item-select.service.ts b/src/app/shared/item-select/item-select.service.ts index 3ba9f9a579..bf4c7ba239 100644 --- a/src/app/shared/item-select/item-select.service.ts +++ b/src/app/shared/item-select/item-select.service.ts @@ -9,8 +9,11 @@ import { } from './item-select.actions'; import { Observable } from 'rxjs/Observable'; import { hasValue } from '../empty.util'; +import { map } from 'rxjs/operators'; +import { AppState } from '../../app.reducer'; -const selectionStateSelector = (state: ItemSelectionsState) => state.selectionItem; +const selectionStateSelector = (state: ItemSelectionsState) => state.itemSelection; +const itemSelectionsStateSelector = (state: AppState) => state.itemSelection; /** * Service that takes care of selecting and deselecting items @@ -18,7 +21,10 @@ const selectionStateSelector = (state: ItemSelectionsState) => state.selectionIt @Injectable() export class ItemSelectService { - constructor(private store: Store) { + constructor( + private store: Store, + private appStore: Store + ) { } /** @@ -27,14 +33,26 @@ export class ItemSelectService { * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false */ getSelected(id: string): Observable { - return this.store.select(selectionByIdSelector(id)) - .map((object: ItemSelectionState) => { + return this.store.select(selectionByIdSelector(id)).pipe( + map((object: ItemSelectionState) => { if (object) { return object.checked; } else { return false; } - }); + }) + ); + } + + /** + * Request the current selection of a given item + * @param {string} id The UUID of the item + * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false + */ + getAllSelected(): Observable { + return this.appStore.select(itemSelectionsStateSelector).pipe( + map((state: ItemSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) + ); } /** From ce134bbd107bb494f51146b9e456b7df2e42fc89 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Sep 2018 11:46:26 +0200 Subject: [PATCH 07/78] 55693: emit only once and reset selected items + notification --- .../collection-item-mapper.component.ts | 11 +++++++---- src/app/shared/item-select/item-select.component.ts | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index f796abc54a..b06f486072 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -15,6 +15,7 @@ import { SearchService } from '../../+search-page/search-service/search.service' import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; @Component({ selector: 'ds-collection-item-mapper', @@ -35,11 +36,11 @@ export class CollectionItemMapperComponent implements OnInit { defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); - constructor(private collectionDataService: CollectionDataService, - private route: ActivatedRoute, + constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, - private searchService: SearchService) { + private searchService: SearchService, + private notificationsService: NotificationsService) { } ngOnInit(): void { @@ -71,7 +72,9 @@ export class CollectionItemMapperComponent implements OnInit { } mapItems(ids: string[]) { - console.log(ids); + this.collectionRD$.subscribe((collectionRD: RemoteData) => { + this.notificationsService.success('Mapping completed', `Successfully mapped ${ids.length} items to collection "${collectionRD.payload.name}".`); + }); } getCurrentUrl(): string { diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index 93128aa1d5..01a4c2e6f1 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs/Observable'; import { Item } from '../../core/shared/item.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { ItemSelectService } from './item-select.service'; +import { take } from 'rxjs/operators'; @Component({ selector: 'ds-item-select', @@ -42,8 +43,11 @@ export class ItemSelectComponent implements OnInit { } confirmSelected() { - this.selectedIds$.subscribe((ids: string[]) => { + this.selectedIds$.pipe( + take(1) + ).subscribe((ids: string[]) => { this.confirm.emit(ids); + this.itemSelectService.reset(); }); } From 4731da83cd31ce5aa36969f259e169ef56e33d35 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Sep 2018 13:16:26 +0200 Subject: [PATCH 08/78] 55693: Intermediate Commit --- .../collection-item-mapper.component.ts | 12 +++++++- src/app/core/data/item-data.service.ts | 30 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index b06f486072..04657fe4fd 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -16,6 +16,8 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { forkJoin } from 'rxjs/observable/forkJoin'; @Component({ selector: 'ds-collection-item-mapper', @@ -40,7 +42,8 @@ export class CollectionItemMapperComponent implements OnInit { private router: Router, private searchConfigService: SearchConfigurationService, private searchService: SearchService, - private notificationsService: NotificationsService) { + private notificationsService: NotificationsService, + private itemDataService: ItemDataService) { } ngOnInit(): void { @@ -72,6 +75,13 @@ export class CollectionItemMapperComponent implements OnInit { } mapItems(ids: string[]) { + const responses = this.collectionRD$.pipe( + map((collectionRD: RemoteData) => collectionRD.payload), + flatMap((collection: Collection) => forkJoin(ids.map((id: string) => this.itemDataService.mapToCollection(id, collection.id)))) + ); + + responses.subscribe((value) => console.log(value)); + this.collectionRD$.subscribe((collectionRD: RemoteData) => { this.notificationsService.success('Mapping completed', `Successfully mapped ${ids.length} items to collection "${collectionRD.payload.name}".`); }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f984dceb12..5b26c8f252 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedItem } from '../cache/models/normalized-item.model'; @@ -15,7 +15,11 @@ 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 } from './request.models'; +import { FindAllOptions, PostRequest, RestRequest } from './request.models'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { configureRequest, getResponseFromSelflink } from '../shared/operators'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { RestResponse } from '../cache/response-cache.models'; @Injectable() export class ItemDataService extends DataService { @@ -48,4 +52,26 @@ export class ItemDataService extends DataService { .distinctUntilChanged(); } + public getMappingCollectionsEndpoint(itemId: string, collectionId?: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)), + map((endpoint: string) => `${endpoint}/mappingCollections${collectionId ? `/${collectionId}` : ''}`) + ); + } + + public mapToCollection(itemId: string, collectionId: string): Observable { + const request$ = this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService) + ); + + return request$.pipe( + map((request: RestRequest) => request.href), + getResponseFromSelflink(this.responseCache), + map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response) + ); + } + } From 7b74d9c07752df3f26e7f58ebd14be03af4818ec Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Sep 2018 17:08:52 +0200 Subject: [PATCH 09/78] 55693: Notifications on success/failure and small refactoring --- .../collection-item-mapper.component.ts | 39 ++++++++++++------- src/app/core/data/item-data.service.ts | 7 +--- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 04657fe4fd..6d250d63eb 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -9,8 +9,8 @@ import { SearchConfigurationService } from '../../+search-page/search-service/se import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; import { Item } from '../../core/shared/item.model'; -import { combineLatest, flatMap, map, tap } from 'rxjs/operators'; -import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; +import { combineLatest, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { filterSuccessfulResponses, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; @@ -18,6 +18,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { forkJoin } from 'rxjs/observable/forkJoin'; +import { RestResponse } from '../../core/cache/response-cache.models'; @Component({ selector: 'ds-collection-item-mapper', @@ -47,12 +48,16 @@ export class CollectionItemMapperComponent implements OnInit { } ngOnInit(): void { - this.collectionRD$ = this.route.data.map((data) => data.collection); + this.collectionRD$ = this.route.data.map((data) => data.collection).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; - this.collectionItemsRD$ = this.collectionRD$.pipe( - getSucceededRemoteData(), - combineLatest(this.searchOptions$), - flatMap(([collectionRD, options]) => { + + const collectionAndOptions$ = Observable.combineLatest( + this.collectionRD$, + this.searchOptions$ + ); + + this.collectionItemsRD$ = collectionAndOptions$.pipe( + switchMap(([collectionRD, options]) => { return this.searchService.search(Object.assign(options, { scope: collectionRD.payload.id, dsoType: DSpaceObjectType.ITEM, @@ -75,15 +80,21 @@ export class CollectionItemMapperComponent implements OnInit { } mapItems(ids: string[]) { - const responses = this.collectionRD$.pipe( - map((collectionRD: RemoteData) => collectionRD.payload), - flatMap((collection: Collection) => forkJoin(ids.map((id: string) => this.itemDataService.mapToCollection(id, collection.id)))) + const responses$ = this.collectionRD$.pipe( + getSucceededRemoteData(), + map((collectionRD: RemoteData) => collectionRD.payload.id), + switchMap((collectionId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.mapToCollection(id, collectionId)))) ); - responses.subscribe((value) => console.log(value)); - - this.collectionRD$.subscribe((collectionRD: RemoteData) => { - this.notificationsService.success('Mapping completed', `Successfully mapped ${ids.length} items to collection "${collectionRD.payload.name}".`); + responses$.subscribe((responses: RestResponse[]) => { + const successful = responses.filter((response: RestResponse) => response.isSuccessful); + const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); + if (successful.length > 0) { + this.notificationsService.success('Mapping completed', `Successfully mapped ${successful.length} items.`); + } + if (unsuccessful.length > 0) { + this.notificationsService.error('Mapping errors', `Errors occurred for mapping of ${unsuccessful.length} items.`); + } }); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 5b26c8f252..7c2c4e572d 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -60,14 +60,11 @@ export class ItemDataService extends DataService { } public mapToCollection(itemId: string, collectionId: string): Observable { - const request$ = this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), - configureRequest(this.requestService) - ); - - return request$.pipe( + configureRequest(this.requestService), map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response) From ba8be3f57d6e507e0e02519e5b7542606df935d0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 28 Sep 2018 10:56:17 +0200 Subject: [PATCH 10/78] 55693: Temporary pagination fix (remove url params on tab change) --- .../collection-item-mapper.component.html | 5 +++-- .../collection-item-mapper.component.ts | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index d201ce0ad6..f18c76f04d 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -15,14 +15,15 @@ - +
+ [objects]="collectionItemsRD$ | async" + [hideGear]="true">
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 6d250d63eb..eab9292e98 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; -import { CollectionDataService } from '../../core/data/collection-data.service'; import { ActivatedRoute, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { Observable } from 'rxjs/Observable'; @@ -8,16 +7,14 @@ import { Collection } from '../../core/shared/collection.model'; import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { Item } from '../../core/shared/item.model'; -import { combineLatest, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; -import { filterSuccessfulResponses, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; +import { flatMap, map, switchMap } from 'rxjs/operators'; +import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ItemDataService } from '../../core/data/item-data.service'; -import { forkJoin } from 'rxjs/observable/forkJoin'; import { RestResponse } from '../../core/cache/response-cache.models'; @Component({ @@ -68,7 +65,6 @@ export class CollectionItemMapperComponent implements OnInit { ); this.mappingItemsRD$ = this.searchOptions$.pipe( flatMap((options: PaginatedSearchOptions) => { - options.sort.field = 'dc.title'; return this.searchService.search(Object.assign(options, { scope: undefined, dsoType: DSpaceObjectType.ITEM, @@ -98,6 +94,15 @@ export class CollectionItemMapperComponent implements OnInit { }); } + tabChange(event) { + // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) + // Temporary solution: Clear url params when changing tabs + if (this.router.url.indexOf('?') > -1) { + const url: string = this.router.url.substring(0, this.router.url.indexOf('?')); + this.router.navigateByUrl(url); + } + } + getCurrentUrl(): string { const urlTree = this.router.parseUrl(this.router.url); const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; From 0a8a6bb7209ceca4c7131e395bade976182fee5a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 28 Sep 2018 11:27:53 +0200 Subject: [PATCH 11/78] 55693: Messages improvement --- resources/i18n/en.json | 11 ++++++++ .../collection-item-mapper.component.html | 6 ++++- .../collection-item-mapper.component.ts | 26 ++++++++++++++++--- .../item-select/item-select.component.html | 2 +- .../item-select/item-select.component.ts | 3 +++ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 948f538e9a..86ff55da66 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -18,10 +18,21 @@ "head": "Item Mapper - Map Items from Other Collections", "collection": "Collection: \"{{name}}\"", "description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "confirm": "Map selected items", "tabs": { "browse": "Browse", "map": "Map" }, + "notifications": { + "success": { + "head": "Mapping completed", + "content": "Successfully mapped {{amount}} items." + }, + "error": { + "head": "Mapping errors", + "content": "Errors occurred for mapping of {{amount}} items." + } + }, "return": "Return" } }, diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index f18c76f04d..88a39f3be9 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -31,7 +31,11 @@
- +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index eab9292e98..806e535e5f 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -16,6 +16,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { RestResponse } from '../../core/cache/response-cache.models'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-collection-item-mapper', @@ -41,18 +42,21 @@ export class CollectionItemMapperComponent implements OnInit { private searchConfigService: SearchConfigurationService, private searchService: SearchService, private notificationsService: NotificationsService, - private itemDataService: ItemDataService) { + private itemDataService: ItemDataService, + private translateService: TranslateService) { } ngOnInit(): void { this.collectionRD$ = this.route.data.map((data) => data.collection).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; + this.loadItemLists(); + } + loadItemLists() { const collectionAndOptions$ = Observable.combineLatest( this.collectionRD$, this.searchOptions$ ); - this.collectionItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options]) => { return this.searchService.search(Object.assign(options, { @@ -86,10 +90,24 @@ export class CollectionItemMapperComponent implements OnInit { const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { - this.notificationsService.success('Mapping completed', `Successfully mapped ${successful.length} items.`); + const successMessages = Observable.combineLatest( + this.translateService.get('collection.item-mapper.notifications.success.head'), + this.translateService.get('collection.item-mapper.notifications.success.content', { amount: successful.length }) + ); + + successMessages.subscribe(([head, content]) => { + this.notificationsService.success(head, content); + }); } if (unsuccessful.length > 0) { - this.notificationsService.error('Mapping errors', `Errors occurred for mapping of ${unsuccessful.length} items.`); + const unsuccessMessages = Observable.combineLatest( + this.translateService.get('collection.item-mapper.notifications.error.head'), + this.translateService.get('collection.item-mapper.notifications.error.content', { amount: unsuccessful.length }) + ); + + unsuccessMessages.subscribe(([head, content]) => { + this.notificationsService.error(head, content); + }); } }); } diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index 4047884bc4..76fc118282 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -26,4 +26,4 @@ - + diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index 01a4c2e6f1..00ca078bfa 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -22,6 +22,9 @@ export class ItemSelectComponent implements OnInit { @Input() paginationOptions: PaginationComponentOptions; + @Input() + confirmButton = 'item.select.confirm'; + @Output() confirm: EventEmitter = new EventEmitter(); From e1c734e1137d605dc81111ad9e65048f7ac09476 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 28 Sep 2018 12:43:38 +0200 Subject: [PATCH 12/78] 55693: CollectionItemMapperComponent test intermediate --- .../collection-item-mapper.component.spec.ts | 83 +++++++++++++++++++ src/app/shared/testing/active-router-stub.ts | 20 ++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index e69de29bb2..18558c6314 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -0,0 +1,83 @@ +import { CollectionItemMapperComponent } from './collection-item-mapper.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonModule } from '@angular/common'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { SearchFormComponent } from '../../shared/search-form/search-form.component'; +import { SearchPageModule } from '../../+search-page/search-page.module'; +import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component'; +import { ItemSelectComponent } from '../../shared/item-select/item-select.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { RouterStub } from '../../shared/testing/router-stub'; +import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; +import { SearchService } from '../../+search-page/search-service/search.service'; +import { SearchServiceStub } from '../../shared/testing/search-service-stub'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../shared/shared.module'; +import { Collection } from '../../core/shared/collection.model'; + +fdescribe('CollectionItemMapperComponent', () => { + let comp: CollectionItemMapperComponent; + let fixture: ComponentFixture; + + let route: ActivatedRoute; + let router: Router; + let searchConfigService: SearchConfigurationService; + let searchService: SearchService; + let notificationsService: NotificationsService; + let itemDataService: ItemDataService; + + const searchConfigServiceStub = { + + }; + const itemDataServiceStub = { + + }; + const mockCollection: Collection = Object.assign(new Collection(), { + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Test title' + }] + }); + const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollection }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [CollectionItemMapperComponent], + providers: [ + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: Router, useValue: new RouterStub() }, + { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, + { provide: SearchService, useValue: new SearchServiceStub() }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: ItemDataService, useValue: itemDataServiceStub }, + { provide: TranslateService, useValue: {} } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionItemMapperComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + route = (comp as any).route; + router = (comp as any).router; + searchConfigService = (comp as any).searchConfigService; + searchService = (comp as any).searchService; + notificationsService = (comp as any).notificationsService; + itemDataService = (comp as any).itemDataService; + }); + + it('should test', () => { + + }); + +}); diff --git a/src/app/shared/testing/active-router-stub.ts b/src/app/shared/testing/active-router-stub.ts index 22c4060855..2ed3f367d3 100644 --- a/src/app/shared/testing/active-router-stub.ts +++ b/src/app/shared/testing/active-router-stub.ts @@ -5,19 +5,27 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject'; export class ActivatedRouteStub { private _testParams?: any; + private _testData?: any; // ActivatedRoute.params is Observable private subject?: BehaviorSubject = new BehaviorSubject(this.testParams); + private dataSubject?: BehaviorSubject = new BehaviorSubject(this.testData); params = this.subject.asObservable(); queryParams = this.subject.asObservable(); queryParamMap = this.subject.asObservable().map((params: Params) => convertToParamMap(params)); + data = this.dataSubject.asObservable(); - constructor(params?: Params) { + constructor(params?: Params, data?: any) { if (params) { this.testParams = params; } else { this.testParams = {}; } + if (data) { + this.testData = data; + } else { + this.testData = {}; + } } // Test parameters @@ -30,6 +38,16 @@ export class ActivatedRouteStub { this.subject.next(params); } + // Test data + get testData() { + return this._testParams; + } + + set testData(data: {}) { + this._testData = data; + this.dataSubject.next(data); + } + // ActivatedRoute.snapshot.params get snapshot() { return { From 95b46352918b391ff611288a1b0d998d3c622a0d Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 28 Sep 2018 16:02:39 +0200 Subject: [PATCH 13/78] 55693: CollectionItemMapperComponent tests --- .../collection-item-mapper.component.html | 2 +- .../collection-item-mapper.component.spec.ts | 84 +++++++++++++++---- .../collection-item-mapper.component.ts | 12 ++- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 88a39f3be9..a7a1416cb0 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -2,7 +2,7 @@

{{'collection.item-mapper.head' | translate}}

-

+

{{'collection.item-mapper.description' | translate}}

diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 18558c6314..a08322dc1b 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -1,5 +1,5 @@ import { CollectionItemMapperComponent } from './collection-item-mapper.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { CommonModule } from '@angular/common'; @@ -20,6 +20,18 @@ import { ItemDataService } from '../../core/data/item-data.service'; import { FormsModule } from '@angular/forms'; import { SharedModule } from '../../shared/shared.module'; import { Collection } from '../../core/shared/collection.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { Observable } from 'rxjs/Observable'; +import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { EventEmitter } from '@angular/core'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { By } from '@angular/platform-browser'; +import { RestResponse } from '../../core/cache/response-cache.models'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; fdescribe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -32,21 +44,39 @@ fdescribe('CollectionItemMapperComponent', () => { let notificationsService: NotificationsService; let itemDataService: ItemDataService; + const mockCollection: Collection = Object.assign(new Collection(), { + id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', + name: 'test-collection' + }); + const mockCollectionRD: RemoteData = new RemoteData(false, false, true, null, mockCollection); + const mockSearchOptions = Observable.of(new PaginatedSearchOptions({ + pagination: Object.assign(new PaginationComponentOptions(), { + id: 'search-page-configuration', + pageSize: 10, + currentPage: 1 + }), + sort: new SortOptions('dc.title', SortDirection.ASC), + scope: mockCollection.id + })); + const routerStub = Object.assign(new RouterStub(), { + url: 'http://test.url' + }); const searchConfigServiceStub = { - + paginatedSearchOptions: mockSearchOptions }; const itemDataServiceStub = { - + mapToCollection: () => Observable.of(new RestResponse(true, '200')) }; - const mockCollection: Collection = Object.assign(new Collection(), { - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Test title' - }] + const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD }); + const translateServiceStub = { + get: () => Observable.of('test-message of collection ' + mockCollection.name), + onLangChange: new EventEmitter(), + onTranslationChange: new EventEmitter(), + onDefaultLangChange: new EventEmitter() + }; + const searchServiceStub = Object.assign(new SearchServiceStub(), { + search: () => Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))) }); - const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollection }); beforeEach(async(() => { TestBed.configureTestingModule({ @@ -54,12 +84,13 @@ fdescribe('CollectionItemMapperComponent', () => { declarations: [CollectionItemMapperComponent], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, - { provide: Router, useValue: new RouterStub() }, + { provide: Router, useValue: routerStub }, { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, - { provide: SearchService, useValue: new SearchServiceStub() }, + { provide: SearchService, useValue: searchServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: ItemDataService, useValue: itemDataServiceStub }, - { provide: TranslateService, useValue: {} } + { provide: TranslateService, useValue: translateServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ] }).compileComponents(); })); @@ -76,8 +107,31 @@ fdescribe('CollectionItemMapperComponent', () => { itemDataService = (comp as any).itemDataService; }); - it('should test', () => { + it('should display the correct collection name', () => { + const name: HTMLElement = fixture.debugElement.query(By.css('#collection-name')).nativeElement; + expect(name.innerHTML).toContain(mockCollection.name); + }); + describe('mapItems', () => { + const ids = ['id1', 'id2', 'id3', 'id4']; + + beforeEach(() => { + spyOn(notificationsService, 'success').and.callThrough(); + spyOn(notificationsService, 'error').and.callThrough(); + }); + + it('should display a success message if at least one mapping was successful', () => { + comp.mapItems(ids); + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + }); + + it('should display an error message if at least one mapping was unsuccessful', () => { + spyOn(itemDataService, 'mapToCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + comp.mapItems(ids); + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); }); }); diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 806e535e5f..9367dbd5b5 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -115,16 +115,14 @@ export class CollectionItemMapperComponent implements OnInit { tabChange(event) { // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) // Temporary solution: Clear url params when changing tabs - if (this.router.url.indexOf('?') > -1) { - const url: string = this.router.url.substring(0, this.router.url.indexOf('?')); - this.router.navigateByUrl(url); - } + this.router.navigateByUrl(this.getCurrentUrl()); } getCurrentUrl(): string { - const urlTree = this.router.parseUrl(this.router.url); - const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET]; - return '/' + g.toString(); + if (this.router.url.indexOf('?') > -1) { + return this.router.url.substring(0, this.router.url.indexOf('?')); + } + return this.router.url; } } From c3add84d860f0761ba334a25cf8303da58c6a365 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 28 Sep 2018 17:31:05 +0200 Subject: [PATCH 14/78] 55693: ItemSelectComponent tests intermediate + ItemSelectServiceStub --- .../collection-item-mapper.component.spec.ts | 2 +- .../item-select/item-select.component.spec.ts | 102 ++++++++++++++++++ .../testing/item-select-service-stub.ts | 37 +++++++ 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/testing/item-select-service-stub.ts diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index a08322dc1b..d7ae41f023 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -33,7 +33,7 @@ import { RestResponse } from '../../core/cache/response-cache.models'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; -fdescribe('CollectionItemMapperComponent', () => { +describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/item-select/item-select.component.spec.ts index e69de29bb2..97132672e2 100644 --- a/src/app/shared/item-select/item-select.component.spec.ts +++ b/src/app/shared/item-select/item-select.component.spec.ts @@ -0,0 +1,102 @@ +import { ItemSelectComponent } from './item-select.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedModule } from '../shared.module'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ItemSelectService } from './item-select.service'; +import { ItemSelectServiceStub } from '../testing/item-select-service-stub'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { Item } from '../../core/shared/item.model'; +import { By } from '@angular/platform-browser'; +import { ActivatedRoute, Route, Router } from '@angular/router'; +import { ActivatedRouteStub } from '../testing/active-router-stub'; +import { RouterStub } from '../testing/router-stub'; +import { HostWindowService } from '../host-window.service'; +import { HostWindowServiceStub } from '../testing/host-window-service-stub'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { LocationStrategy } from '@angular/common'; +import { MockLocationStrategy } from '@angular/common/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +fdescribe('ItemSelectComponent', () => { + let comp: ItemSelectComponent; + let fixture: ComponentFixture; + let itemSelectService: ItemSelectService; + + const mockItemList = [ + Object.assign(new Item(), { + id: 'id1', + bitstreams: Observable.of({}), + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'This is just a title' + }, + { + key: 'dc.type', + language: null, + value: 'Article' + }] + }), + Object.assign(new Item(), { + id: 'id2', + bitstreams: Observable.of({}), + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'This is just another title' + }, + { + key: 'dc.type', + language: null, + value: 'Article' + }] + }) + ]; + const mockCheckedItems= [mockItemList[0].id]; + const mockItems = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); + const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-page-configuration', + pageSize: 10, + currentPage: 1 + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], + declarations: [], + providers: [ + { provide: ItemSelectService, useValue: new ItemSelectServiceStub(mockCheckedItems) }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemSelectComponent); + comp = fixture.componentInstance; + comp.itemsRD$ = mockItems; + comp.paginationOptions = mockPaginationOptions; + fixture.detectChanges(); + itemSelectService = (comp as any).itemSelectService; + }); + + it(`should show a list of ${mockItemList.length} items`, () => { + const tbody: HTMLElement = fixture.debugElement.query(By.css('table#item-select tbody')).nativeElement; + expect(tbody.children.length).toBe(mockItemList.length); + }); + + it('should have the correct items selected', () => { + for (const item of mockItemList) { + const checked = (mockCheckedItems.indexOf(item.id) > -1); + const checkbox: HTMLElement = fixture.debugElement.query(By.css(`input[name=${item.id}]`)).nativeElement; + expect(checkbox.getAttribute('checked')).toBe(checked); + } + }); +}); diff --git a/src/app/shared/testing/item-select-service-stub.ts b/src/app/shared/testing/item-select-service-stub.ts new file mode 100644 index 0000000000..690d1e1435 --- /dev/null +++ b/src/app/shared/testing/item-select-service-stub.ts @@ -0,0 +1,37 @@ +import { Observable } from 'rxjs/Observable'; + +export class ItemSelectServiceStub { + + ids: string[] = []; + + constructor(ids?: string[]) { + if (ids) { + this.ids = ids; + } + } + + getSelected(id: string): Observable { + if (this.ids.indexOf(id) > -1) { + return Observable.of(true); + } else { + return Observable.of(false); + } + } + + getAllSelected(): Observable { + return Observable.of(this.ids); + } + + switch(id: string) { + const index = this.ids.indexOf(id); + if (index > -1) { + this.ids.splice(index, 1); + } else { + this.ids.push(id); + } + } + + reset() { + this.ids = []; + } +} From 8b99ea44b2f1edb66305e7af6b533bade4064aaf Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 1 Oct 2018 10:30:05 +0200 Subject: [PATCH 15/78] ItemSelectComponent tests --- .../item-select/item-select.component.html | 4 +- .../item-select/item-select.component.spec.ts | 47 +++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index 76fc118282..9c08cfae87 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -17,7 +17,7 @@ - + {{(item.owningCollection | async)?.payload?.name}} {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}} @@ -26,4 +26,4 @@
- + diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/item-select/item-select.component.spec.ts index 97132672e2..4ca4317eba 100644 --- a/src/app/shared/item-select/item-select.component.spec.ts +++ b/src/app/shared/item-select/item-select.component.spec.ts @@ -1,5 +1,5 @@ import { ItemSelectComponent } from './item-select.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { SharedModule } from '../shared.module'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -58,7 +58,6 @@ fdescribe('ItemSelectComponent', () => { }] }) ]; - const mockCheckedItems= [mockItemList[0].id]; const mockItems = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', @@ -71,7 +70,7 @@ fdescribe('ItemSelectComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ - { provide: ItemSelectService, useValue: new ItemSelectServiceStub(mockCheckedItems) }, + { provide: ItemSelectService, useValue: new ItemSelectServiceStub() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [NO_ERRORS_SCHEMA] @@ -92,11 +91,41 @@ fdescribe('ItemSelectComponent', () => { expect(tbody.children.length).toBe(mockItemList.length); }); - it('should have the correct items selected', () => { - for (const item of mockItemList) { - const checked = (mockCheckedItems.indexOf(item.id) > -1); - const checkbox: HTMLElement = fixture.debugElement.query(By.css(`input[name=${item.id}]`)).nativeElement; - expect(checkbox.getAttribute('checked')).toBe(checked); - } + describe('checkboxes', () => { + let checkbox: HTMLInputElement; + + beforeEach(() => { + checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement; + }); + + it('should initially be unchecked',() => { + expect(checkbox.checked).toBeFalsy(); + }); + + it('should be checked when clicked', () => { + checkbox.click(); + fixture.detectChanges(); + expect(checkbox.checked).toBeTruthy(); + }); + + it('should switch the value through item-select-service', () => { + spyOn((comp as any).itemSelectService, 'switch').and.callThrough(); + checkbox.click(); + expect((comp as any).itemSelectService.switch).toHaveBeenCalled(); + }); + }); + + describe('when confirm is clicked', () => { + let confirmButton: HTMLButtonElement; + + beforeEach(() => { + confirmButton = fixture.debugElement.query(By.css('button.item-confirm')).nativeElement; + spyOn(comp.confirm, 'emit').and.callThrough(); + }); + + it('should emit the selected items',() => { + confirmButton.click(); + expect(comp.confirm.emit).toHaveBeenCalled(); + }); }); }); From 7934b87336ba9390f83bf7f20cd662c904c48871 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 1 Oct 2018 11:38:13 +0200 Subject: [PATCH 16/78] 55693: ItemSelectService tests --- .../item-select/item-select.component.spec.ts | 2 +- .../item-select/item-select.service.spec.ts | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/item-select/item-select.service.spec.ts diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/item-select/item-select.component.spec.ts index 4ca4317eba..0f3a9d5fae 100644 --- a/src/app/shared/item-select/item-select.component.spec.ts +++ b/src/app/shared/item-select/item-select.component.spec.ts @@ -21,7 +21,7 @@ import { LocationStrategy } from '@angular/common'; import { MockLocationStrategy } from '@angular/common/testing'; import { RouterTestingModule } from '@angular/router/testing'; -fdescribe('ItemSelectComponent', () => { +describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; let itemSelectService: ItemSelectService; diff --git a/src/app/shared/item-select/item-select.service.spec.ts b/src/app/shared/item-select/item-select.service.spec.ts new file mode 100644 index 0000000000..f7b28a5b04 --- /dev/null +++ b/src/app/shared/item-select/item-select.service.spec.ts @@ -0,0 +1,96 @@ +import { ItemSelectService } from './item-select.service'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { ItemSelectionsState } from './item-select.reducer'; +import { AppState } from '../../app.reducer'; +import { + ItemSelectionDeselectAction, + ItemSelectionInitialDeselectAction, + ItemSelectionInitialSelectAction, ItemSelectionResetAction, + ItemSelectionSelectAction, ItemSelectionSwitchAction +} from './item-select.actions'; + +describe('ItemSelectService', () => { + let service: ItemSelectService; + + const mockItemId = 'id1'; + + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + const appStore: Store = jasmine.createSpyObj('appStore', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + beforeEach(() => { + service = new ItemSelectService(store, appStore); + }); + + describe('when the initialSelect method is triggered', () => { + beforeEach(() => { + service.initialSelect(mockItemId); + }); + + it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialSelectAction(mockItemId)); + }); + }); + + describe('when the initialDeselect method is triggered', () => { + beforeEach(() => { + service.initialDeselect(mockItemId); + }); + + it('ItemSelectionInitialDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialDeselectAction(mockItemId)); + }); + }); + + describe('when the select method is triggered', () => { + beforeEach(() => { + service.select(mockItemId); + }); + + it('ItemSelectionSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSelectAction(mockItemId)); + }); + }); + + describe('when the deselect method is triggered', () => { + beforeEach(() => { + service.deselect(mockItemId); + }); + + it('ItemSelectionDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionDeselectAction(mockItemId)); + }); + }); + + describe('when the switch method is triggered', () => { + beforeEach(() => { + service.switch(mockItemId); + }); + + it('ItemSelectionSwitchAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSwitchAction(mockItemId)); + }); + }); + + describe('when the reset method is triggered', () => { + beforeEach(() => { + service.reset(); + }); + + it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionResetAction(null)); + }); + }); + +}); From 199e6c729943b74554ff1cf749b259d991a16f86 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 1 Oct 2018 11:43:37 +0200 Subject: [PATCH 17/78] 55693: Empty ItemSelectReducer test file --- .../shared/item-select/item-select.reducer.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/app/shared/item-select/item-select.reducer.spec.ts diff --git a/src/app/shared/item-select/item-select.reducer.spec.ts b/src/app/shared/item-select/item-select.reducer.spec.ts new file mode 100644 index 0000000000..3067a4e93a --- /dev/null +++ b/src/app/shared/item-select/item-select.reducer.spec.ts @@ -0,0 +1,13 @@ +import { ItemSelectionSelectAction } from './item-select.actions'; + +class NullAction extends ItemSelectionSelectAction { + type = null; + + constructor() { + super(undefined); + } +} + +fdescribe('itemSelectionReducer', () => { + +}); From 5f56d5959da5b86db7a32c7117ce8a8f2edb1d5e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 1 Oct 2018 13:40:29 +0200 Subject: [PATCH 18/78] 55693: ItemSelectReducer tests --- .../item-select/item-select.reducer.spec.ts | 89 ++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/app/shared/item-select/item-select.reducer.spec.ts b/src/app/shared/item-select/item-select.reducer.spec.ts index 3067a4e93a..10c5db9b7f 100644 --- a/src/app/shared/item-select/item-select.reducer.spec.ts +++ b/src/app/shared/item-select/item-select.reducer.spec.ts @@ -1,4 +1,12 @@ -import { ItemSelectionSelectAction } from './item-select.actions'; +import { + ItemSelectionDeselectAction, ItemSelectionInitialDeselectAction, + ItemSelectionInitialSelectAction, ItemSelectionResetAction, + ItemSelectionSelectAction, ItemSelectionSwitchAction +} from './item-select.actions'; +import { itemSelectionReducer } from './item-select.reducer'; + +const itemId1 = 'id1'; +const itemId2 = 'id2'; class NullAction extends ItemSelectionSelectAction { type = null; @@ -8,6 +16,83 @@ class NullAction extends ItemSelectionSelectAction { } } -fdescribe('itemSelectionReducer', () => { +describe('itemSelectionReducer', () => { + + it('should return the current state when no valid actions have been made', () => { + const state = {}; + state[itemId1] = { checked: true }; + const action = new NullAction(); + const newState = itemSelectionReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an empty object', () => { + const state = {}; + const action = new NullAction(); + const newState = itemSelectionReducer(undefined, action); + + expect(newState).toEqual(state); + }); + + it('should set checked to true in response to the INITIAL_SELECT action', () => { + const action = new ItemSelectionInitialSelectAction(itemId1); + const newState = itemSelectionReducer(undefined, action); + + expect(newState[itemId1].checked).toBeTruthy(); + }); + + it('should set checked to true in response to the INITIAL_DESELECT action', () => { + const action = new ItemSelectionInitialDeselectAction(itemId1); + const newState = itemSelectionReducer(undefined, action); + + expect(newState[itemId1].checked).toBeFalsy(); + }); + + it('should set checked to true in response to the SELECT action', () => { + const state = {}; + state[itemId1] = { checked: false }; + const action = new ItemSelectionSelectAction(itemId1); + const newState = itemSelectionReducer(state, action); + + expect(newState[itemId1].checked).toBeTruthy(); + }); + + it('should set checked to false in response to the DESELECT action', () => { + const state = {}; + state[itemId1] = { checked: true }; + const action = new ItemSelectionDeselectAction(itemId1); + const newState = itemSelectionReducer(state, action); + + expect(newState[itemId1].checked).toBeFalsy(); + }); + + it('should set checked from false to true in response to the SWITCH action', () => { + const state = {}; + state[itemId1] = { checked: false }; + const action = new ItemSelectionSwitchAction(itemId1); + const newState = itemSelectionReducer(state, action); + + expect(newState[itemId1].checked).toBeTruthy(); + }); + + it('should set checked from true to false in response to the SWITCH action', () => { + const state = {}; + state[itemId1] = { checked: true }; + const action = new ItemSelectionSwitchAction(itemId1); + const newState = itemSelectionReducer(state, action); + + expect(newState[itemId1].checked).toBeFalsy(); + }); + + it('should set reset the state in response to the RESET action', () => { + const state = {}; + state[itemId1] = { checked: true }; + state[itemId2] = { checked: false }; + const action = new ItemSelectionResetAction(undefined); + const newState = itemSelectionReducer(state, action); + + expect(newState).toEqual({}); + }); }); From 24f6f982e90bd43fa0434674edcafaa7834c93b0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 1 Oct 2018 14:14:26 +0200 Subject: [PATCH 19/78] 55693: Authentication Guard and TSDocs --- .../collection-item-mapper.component.ts | 41 +++++++++++++++++++ .../collection-page-routing.module.ts | 4 +- .../item-select/item-select.component.ts | 33 +++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 9367dbd5b5..15f7db63b4 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -28,13 +28,37 @@ import { TranslateService } from '@ngx-translate/core'; fadeInOut ] }) +/** + * Collection used to map items to a collection + */ export class CollectionItemMapperComponent implements OnInit { + /** + * The collection to map items to + */ collectionRD$: Observable>; + + /** + * Search options + */ searchOptions$: Observable; + + /** + * List of items to show under the "Browse" tab + * Items inside the collection + */ collectionItemsRD$: Observable>>; + + /** + * List of items to show under the "Map" tab + * Items outside the collection + */ mappingItemsRD$: Observable>>; + /** + * Sort on title ASC by default + * @type {SortOptions} + */ defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); constructor(private route: ActivatedRoute, @@ -52,6 +76,11 @@ export class CollectionItemMapperComponent implements OnInit { this.loadItemLists(); } + /** + * Load collectionItemsRD$ with a fixed scope to only obtain the items this collection owns + * Load mappingItemsRD$ to only obtain items this collection doesn't own + * TODO: When the API support it, fetch items excluding the collection's scope (currently fetches all items) + */ loadItemLists() { const collectionAndOptions$ = Observable.combineLatest( this.collectionRD$, @@ -79,6 +108,10 @@ export class CollectionItemMapperComponent implements OnInit { ); } + /** + * Map the selected items to the collection and display notifications + * @param {string[]} ids The list of item UUID's to map to the collection + */ mapItems(ids: string[]) { const responses$ = this.collectionRD$.pipe( getSucceededRemoteData(), @@ -112,12 +145,20 @@ export class CollectionItemMapperComponent implements OnInit { }); } + /** + * Clear url parameters on tab change (temporary fix until pagination is improved) + * @param event + */ tabChange(event) { // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) // Temporary solution: Clear url params when changing tabs this.router.navigateByUrl(this.getCurrentUrl()); } + /** + * Get current url without parameters + * @returns {string} + */ getCurrentUrl(): string { if (this.router.url.indexOf('?') > -1) { return this.router.url.substring(0, this.router.url.indexOf('?')); diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index c85d102437..f04d40e234 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router'; import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageResolver } from './collection-page.resolver'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @NgModule({ imports: [ @@ -22,7 +23,8 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti pathMatch: 'full', resolve: { collection: CollectionPageResolver - } + }, + canActivate: [AuthenticatedGuard] } ]) ], diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index 00ca078bfa..3a2003a327 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -14,20 +14,40 @@ import { take } from 'rxjs/operators'; templateUrl: './item-select.component.html' }) +/** + * A component used to select items from a specific list and returning the UUIDs of the selected items + */ export class ItemSelectComponent implements OnInit { + /** + * The list of items to display + */ @Input() itemsRD$: Observable>>; + /** + * The pagination options used to display the items + */ @Input() paginationOptions: PaginationComponentOptions; + /** + * The message key used for the confirm button + * @type {string} + */ @Input() confirmButton = 'item.select.confirm'; + /** + * EventEmitter to return the selected UUIDs when the confirm button is pressed + * @type {EventEmitter} + */ @Output() confirm: EventEmitter = new EventEmitter(); + /** + * The list of selected UUIDs + */ selectedIds$: Observable; constructor(private itemSelectService: ItemSelectService) { @@ -37,14 +57,27 @@ export class ItemSelectComponent implements OnInit { this.selectedIds$ = this.itemSelectService.getAllSelected(); } + /** + * Switch the state of a checkbox + * @param {string} id + */ switch(id: string) { this.itemSelectService.switch(id); } + /** + * Get the current state of a checkbox + * @param {string} id The item's UUID + * @returns {Observable} + */ getSelected(id: string): Observable { return this.itemSelectService.getSelected(id); } + /** + * Called when the confirm button is pressed + * Sends the selected UUIDs to the parent component + */ confirmSelected() { this.selectedIds$.pipe( take(1) From ba4d2861f5d95fd827c0e0c6de5731314a0074ee Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Oct 2018 17:50:52 +0200 Subject: [PATCH 20/78] 55946: Start of Edit Item Page --- resources/i18n/en.json | 53 ++++++++++++++++++ .../edit-item-page.component.html | 52 ++++++++++++++++++ .../edit-item-page.component.scss | 0 .../edit-item-page.component.spec.ts | 0 .../edit-item-page.component.ts | 55 +++++++++++++++++++ .../+item-page/item-page-routing.module.ts | 10 ++++ src/app/+item-page/item-page.module.ts | 4 +- 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.html create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.scss create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 86ff55da66..7c45b612f7 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -72,6 +72,59 @@ "title": "Title" }, "confirm": "Confirm selected" + }, + "edit": { + "head": "Edit Item", + "tabs": { + "status": { + "head": "Item Status", + "description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", + "labels": { + "id": "Item Internal ID", + "handle": "Handle", + "lastModified": "Last Modified", + "itemPage": "Item Page" + }, + "buttons": { + "authorizations": { + "label": "Edit item's authorization policies", + "button": "Authorizations..." + }, + "withdraw": { + "label": "Withdraw item from the repository", + "button": "Withdraw..." + }, + "move": { + "label": "Move item to another collection", + "button": "Move..." + }, + "private": { + "label": "Make item private", + "button": "Make it private..." + }, + "delete": { + "label": "Completely expunge item", + "button": "Permanently delete" + }, + "mapped-collections": { + "label": "Manage mapped collections", + "button": "Mapped collections" + } + } + }, + "bitstreams": { + "head": "Item Bitstreams" + }, + "metadata": { + "head": "Item Metadata" + }, + "view": { + "head": "View Item" + }, + "curate": { + "head": "Curate" + } + } } }, "nav": { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.html b/src/app/+item-page/edit-item-page/edit-item-page.component.html new file mode 100644 index 0000000000..ca15e83ab9 --- /dev/null +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.html @@ -0,0 +1,52 @@ +
+
+
+

{{'item.edit.head' | translate}}

+
+ + + +

{{'item.edit.tabs.status.description' | translate}}

+
+
+
+ {{'item.edit.tabs.status.labels.' + statusKey | translate}}: +
+
+ {{(statusData$ | async)[statusKey]}} +
+
+
+ {{'item.edit.tabs.status.labels.itemPage' | translate}}: +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.scss b/src/app/+item-page/edit-item-page/edit-item-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.spec.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts new file mode 100644 index 0000000000..eb017669fc --- /dev/null +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -0,0 +1,55 @@ +import { fadeIn, fadeInOut } from '../../shared/animations/fade'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { getSucceededRemoteData } from '../../core/shared/operators'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'ds-edit-item-page', + styleUrls: ['./edit-item-page.component.scss'], + templateUrl: './edit-item-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeIn, + fadeInOut + ] +}) +/** + * Page component for editing an item + */ +export class EditItemPageComponent implements OnInit { + + objectKeys = Object.keys; + + /** + * The item to edit + */ + itemRD$: Observable>; + statusData$: Observable; + + constructor(private route: ActivatedRoute, + private router: Router) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.map((data) => data.item); + this.statusData$ = this.itemRD$.pipe( + getSucceededRemoteData(), + map((itemRD: RemoteData) => itemRD.payload), + map((item: Item) => Object.assign({ + id: item.id, + handle: item.handle, + lastModified: item.lastModified + })) + ) + } + + getItemPage(): string { + return this.router.url.substr(0, this.router.url.lastIndexOf('/')); + } + +} diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 96158b867e..0105c947e9 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -4,6 +4,8 @@ import { RouterModule } from '@angular/router'; import { ItemPageComponent } from './simple/item-page.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { ItemPageResolver } from './item-page.resolver'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { EditItemPageComponent } from './edit-item-page/edit-item-page.component'; @NgModule({ imports: [ @@ -22,6 +24,14 @@ import { ItemPageResolver } from './item-page.resolver'; resolve: { item: ItemPageResolver } + }, + { + path: ':id/edit', + component: EditItemPageComponent, + resolve: { + item: ItemPageResolver + }, + canActivate: [AuthenticatedGuard] } ]) ], diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index bd801923e3..45251bb1d8 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -18,6 +18,7 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; +import { EditItemPageComponent } from './edit-item-page/edit-item-page.component'; @NgModule({ imports: [ @@ -39,7 +40,8 @@ import { FullFileSectionComponent } from './full/field-components/file-section/f ItemPageSpecificFieldComponent, FileSectionComponent, CollectionsComponent, - FullFileSectionComponent + FullFileSectionComponent, + EditItemPageComponent ] }) export class ItemPageModule { From 3a809cc6710eb9e54d1bc7c49a2d41c321013c87 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 11:51:52 +0200 Subject: [PATCH 21/78] 55946: Finished edit-item-page and start of collection mapper --- resources/i18n/en.json | 2 +- .../edit-item-page.component.html | 18 +--- .../edit-item-page.component.ts | 24 +----- .../edit-item-page/edit-item-page.module.ts | 23 ++++++ .../edit-item-page.routing.module.ts | 32 ++++++++ .../item-collection-mapper.component.html | 7 ++ .../item-collection-mapper.component.scss | 0 .../item-collection-mapper.component.spec.ts | 0 .../item-collection-mapper.component.ts | 19 +++++ .../item-status/item-status.component.html | 30 +++++++ .../item-status/item-status.component.scss | 0 .../item-status/item-status.component.spec.ts | 0 .../item-status/item-status.component.ts | 82 +++++++++++++++++++ .../+item-page/item-page-routing.module.ts | 6 +- src/app/+item-page/item-page.module.ts | 6 +- 15 files changed, 201 insertions(+), 48 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.module.ts create mode 100644 src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts create mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html create mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.scss create mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts create mode 100644 src/app/+item-page/edit-item-page/item-status/item-status.component.html create mode 100644 src/app/+item-page/edit-item-page/item-status/item-status.component.scss create mode 100644 src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-status/item-status.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 7c45b612f7..69c4a4174b 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -106,7 +106,7 @@ "label": "Completely expunge item", "button": "Permanently delete" }, - "mapped-collections": { + "mappedCollections": { "label": "Manage mapped collections", "button": "Mapped collections" } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.html b/src/app/+item-page/edit-item-page/edit-item-page.component.html index ca15e83ab9..001b484c2c 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.html +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.html @@ -6,23 +6,7 @@ -

{{'item.edit.tabs.status.description' | translate}}

-
-
-
- {{'item.edit.tabs.status.labels.' + statusKey | translate}}: -
-
- {{(statusData$ | async)[statusKey]}} -
-
-
- {{'item.edit.tabs.status.labels.itemPage' | translate}}: -
- -
+
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index eb017669fc..7702fc94e8 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -1,12 +1,9 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { ItemDataService } from '../../core/data/item-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import { map } from 'rxjs/operators'; @Component({ selector: 'ds-edit-item-page', @@ -23,33 +20,16 @@ import { map } from 'rxjs/operators'; */ export class EditItemPageComponent implements OnInit { - objectKeys = Object.keys; - /** * The item to edit */ itemRD$: Observable>; - statusData$: Observable; - constructor(private route: ActivatedRoute, - private router: Router) { + constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.itemRD$ = this.route.data.map((data) => data.item); - this.statusData$ = this.itemRD$.pipe( - getSucceededRemoteData(), - map((itemRD: RemoteData) => itemRD.payload), - map((item: Item) => Object.assign({ - id: item.id, - handle: item.handle, - lastModified: item.lastModified - })) - ) - } - - getItemPage(): string { - return this.router.url.substr(0, this.router.url.lastIndexOf('/')); } } 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 new file mode 100644 index 0000000000..70f6fc7d3b --- /dev/null +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; +import { EditItemPageComponent } from './edit-item-page.component'; +import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; +import { ItemStatusComponent } from './item-status/item-status.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + EditItemPageRoutingModule + ], + declarations: [ + EditItemPageComponent, + ItemStatusComponent, + ItemCollectionMapperComponent + ] +}) +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 new file mode 100644 index 0000000000..f2209cddcc --- /dev/null +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -0,0 +1,32 @@ +import { ItemPageResolver } from '../item-page.resolver'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { EditItemPageComponent } from './edit-item-page.component'; +import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: EditItemPageComponent, + resolve: { + item: ItemPageResolver + } + }, + { + path: 'map', + component: ItemCollectionMapperComponent, + resolve: { + item: ItemPageResolver + } + } + ]) + ], + providers: [ + ItemPageResolver, + ] +}) +export class EditItemPageRoutingModule { + +} diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html new file mode 100644 index 0000000000..3fb829fe8b --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -0,0 +1,7 @@ +
+
+
+

It works!

+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.scss b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts new file mode 100644 index 0000000000..592e3bd26c --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; + +@Component({ + selector: 'ds-item-collection-mapper', + styleUrls: ['./item-collection-mapper.component.scss'], + templateUrl: './item-collection-mapper.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeIn, + fadeInOut + ] +}) +/** + * Component for mapping collections to an item + */ +export class ItemCollectionMapperComponent { + +} diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.html b/src/app/+item-page/edit-item-page/item-status/item-status.component.html new file mode 100644 index 0000000000..0a93e7659d --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.html @@ -0,0 +1,30 @@ +

{{'item.edit.tabs.status.description' | translate}}

+
+
+
+ {{'item.edit.tabs.status.labels.' + statusKey | translate}}: +
+
+ {{statusData[statusKey]}} +
+
+
+ {{'item.edit.tabs.status.labels.itemPage' | translate}}: +
+ + +
+
+ + {{'item.edit.tabs.status.buttons.' + actionKey + '.label' | translate}} + +
+ +
+
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.scss b/src/app/+item-page/edit-item-page/item-status/item-status.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..715614c1d9 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; +import { Item } from '../../../core/shared/item.model'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'ds-item-status', + styleUrls: ['./item-status.component.scss'], + templateUrl: './item-status.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + fadeIn, + fadeInOut + ] +}) +/** + * Component for displaying an item's status + */ +export class ItemStatusComponent implements OnInit { + + /** + * The item to display the status for + */ + @Input() item: Item; + + /** + * The data to show in the status + */ + statusData: any; + /** + * The keys of the data (to loop over) + */ + statusDataKeys; + + /** + * The possible actions that can be performed on the item + * key: id value: url to action's component + */ + actions: any; + /** + * The keys of the actions (to loop over) + */ + actionsKeys; + + constructor(private router: Router) { + } + + ngOnInit(): void { + this.statusData = Object.assign({ + id: this.item.id, + handle: this.item.handle, + lastModified: this.item.lastModified + }); + this.statusDataKeys = Object.keys(this.statusData); + + this.actions = Object.assign({ + mappedCollections: this.getCurrentUrl() + '/map' + }); + this.actionsKeys = Object.keys(this.actions); + } + + /** + * Get the url to the simple item page + * @returns {string} url + */ + getItemPage(): string { + return this.router.url.substr(0, this.router.url.lastIndexOf('/')); + } + + /** + * Get the current url without query params + * @returns {string} url + */ + getCurrentUrl(): string { + if (this.router.url.indexOf('?') > -1) { + return this.router.url.substr(0, this.router.url.indexOf('?')); + } else { + return this.router.url; + } + } + +} diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 0105c947e9..be31b0a82d 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -5,7 +5,6 @@ import { ItemPageComponent } from './simple/item-page.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { ItemPageResolver } from './item-page.resolver'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { EditItemPageComponent } from './edit-item-page/edit-item-page.component'; @NgModule({ imports: [ @@ -27,10 +26,7 @@ import { EditItemPageComponent } from './edit-item-page/edit-item-page.component }, { path: ':id/edit', - component: EditItemPageComponent, - resolve: { - item: ItemPageResolver - }, + loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', canActivate: [AuthenticatedGuard] } ]) diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 45251bb1d8..d383189a9c 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -18,12 +18,13 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; -import { EditItemPageComponent } from './edit-item-page/edit-item-page.component'; +import { EditItemPageModule } from './edit-item-page/edit-item-page.module'; @NgModule({ imports: [ CommonModule, SharedModule, + EditItemPageModule, ItemPageRoutingModule ], declarations: [ @@ -40,8 +41,7 @@ import { EditItemPageComponent } from './edit-item-page/edit-item-page.component ItemPageSpecificFieldComponent, FileSectionComponent, CollectionsComponent, - FullFileSectionComponent, - EditItemPageComponent + FullFileSectionComponent ] }) export class ItemPageModule { From 6bc4d061f10bd49bb6c1a5ef3dbe914abc30e858 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 14:09:56 +0200 Subject: [PATCH 22/78] 55946: Intermediate commit --- resources/i18n/en.json | 21 +++++ .../edit-item-page/edit-item-page.module.ts | 2 + .../edit-item-page.routing.module.ts | 2 +- .../item-collection-mapper.component.html | 33 ++++++- .../item-collection-mapper.component.ts | 94 ++++++++++++++++++- .../item-status/item-status.component.ts | 2 +- src/app/core/core.module.ts | 2 + src/app/core/data/item-data.service.ts | 30 +++++- ...ing-collections-reponse-parsing.service.ts | 24 +++++ src/app/core/data/request.models.ts | 7 ++ 10 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 src/app/core/data/mapping-collections-reponse-parsing.service.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 69c4a4174b..f1e6c2b9ed 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -124,6 +124,27 @@ "curate": { "head": "Curate" } + }, + "item-mapper": { + "head": "Item Mapper - Map Item to Collections", + "item": "Item: \"{{name}}\"", + "description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", + "confirm": "Map item to selected collections", + "tabs": { + "browse": "Browse", + "map": "Map" + }, + "notifications": { + "success": { + "head": "Mapping completed", + "content": "Successfully mapped item to {{amount}} collections." + }, + "error": { + "head": "Mapping errors", + "content": "Errors occurred for mapping of item to {{amount}} collections." + } + }, + "return": "Return" } } }, 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 70f6fc7d3b..d11eb7ec7d 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 @@ -5,11 +5,13 @@ import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; import { EditItemPageComponent } from './edit-item-page.component'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemStatusComponent } from './item-status/item-status.component'; +import { SearchPageModule } from '../../+search-page/search-page.module'; @NgModule({ imports: [ CommonModule, SharedModule, + SearchPageModule, EditItemPageRoutingModule ], declarations: [ 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 f2209cddcc..dc3f975468 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 @@ -15,7 +15,7 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col } }, { - path: 'map', + path: 'mapper', component: ItemCollectionMapperComponent, resolve: { item: ItemPageResolver diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 3fb829fe8b..21149eafaf 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -1,7 +1,38 @@
-

It works!

+

{{'item.edit.item-mapper.head' | translate}}

+

+

{{'item.edit.item-mapper.description' | translate}}

+ +
+
+ + +
+
+ + + + +
+
{{col.name}}
+
+
+
+ + +
+ +
+
+
+
+ +
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 592e3bd26c..eeb602292d 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -1,5 +1,20 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; +import { Observable } from 'rxjs/Observable'; +import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { Collection } from '../../../core/shared/collection.model'; +import { Item } from '../../../core/shared/item.model'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SearchService } from '../../../+search-page/search-service/search.service'; +import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; +import { map, switchMap } from 'rxjs/operators'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; @Component({ selector: 'ds-item-collection-mapper', @@ -14,6 +29,81 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; /** * Component for mapping collections to an item */ -export class ItemCollectionMapperComponent { +export class ItemCollectionMapperComponent implements OnInit { + /** + * The item to map to collections + */ + itemRD$: Observable>; + + /** + * Search options + */ + searchOptions$: Observable; + + /** + * List of collections to show under the "Browse" tab + * Collections that are mapped to the item + */ + itemCollectionsRD$: Observable>; + + /** + * List of collections to show under the "Map" tab + * Collections that are not mapped to the item + */ + mappingCollectionsRD$: Observable>>; + + /** + * Sort on title ASC by default + * @type {SortOptions} + */ + defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); + + constructor(private route: ActivatedRoute, + private router: Router, + private searchConfigService: SearchConfigurationService, + private searchService: SearchService, + private collectionDataService: CollectionDataService, + private itemDataService: ItemDataService) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; + this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; + this.loadCollectionLists(); + } + + /** + * Load itemCollectionsRD$ with a fixed scope to only obtain the collections that own this item + * Load mappingCollectionsRD$ to only obtain collections that don't own this item + * TODO: When the API support it, fetch collections excluding the item's scope (currently fetches all collections) + */ + loadCollectionLists() { + this.itemCollectionsRD$ = this.itemRD$.pipe( + map((itemRD: RemoteData) => itemRD.payload), + switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) + ); + this.mappingCollectionsRD$ = this.collectionDataService.findAll(); + } + + /** + * Clear url parameters on tab change (temporary fix until pagination is improved) + * @param event + */ + tabChange(event) { + // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) + // Temporary solution: Clear url params when changing tabs + this.router.navigateByUrl(this.getCurrentUrl()); + } + + /** + * Get current url without parameters + * @returns {string} + */ + getCurrentUrl(): string { + if (this.router.url.indexOf('?') > -1) { + return this.router.url.substring(0, this.router.url.indexOf('?')); + } + return this.router.url; + } } 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 715614c1d9..545ca17b6e 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 @@ -54,7 +54,7 @@ export class ItemStatusComponent implements OnInit { this.statusDataKeys = Object.keys(this.statusData); this.actions = Object.assign({ - mappedCollections: this.getCurrentUrl() + '/map' + mappedCollections: this.getCurrentUrl() + '/mapper' }); this.actionsKeys = Object.keys(this.actions); } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 31b9b31244..4ea3aa8df7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -65,6 +65,7 @@ import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { ItemSelectService } from '../shared/item-select/item-select.service'; +import { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service'; const IMPORTS = [ CommonModule, @@ -111,6 +112,7 @@ const PROVIDERS = [ RegistryMetadataschemasResponseParsingService, RegistryMetadatafieldsResponseParsingService, RegistryBitstreamformatsResponseParsingService, + MappingCollectionsReponseParsingService, MetadataschemaParsingService, DebugResponseParsingService, SearchResponseParsingService, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 7c2c4e572d..250d8c1303 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { isEmpty, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { ensureArrayHasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedItem } from '../cache/models/normalized-item.model'; @@ -15,11 +15,21 @@ 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, PostRequest, RestRequest } from './request.models'; +import { FindAllOptions, GetRequest, MappingCollectionsRequest, PostRequest, RestRequest } from './request.models'; import { distinctUntilChanged, map } from 'rxjs/operators'; -import { configureRequest, getResponseFromSelflink } from '../shared/operators'; +import { + configureRequest, + filterSuccessfulResponses, + getRequestFromSelflink, + getResponseFromSelflink +} from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { RestResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { BrowseDefinition } from '../shared/browse-definition.model'; +import { Collection } from '../shared/collection.model'; +import { NormalizedCollection } from '../cache/models/normalized-collection.model'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; @Injectable() export class ItemDataService extends DataService { @@ -71,4 +81,16 @@ export class ItemDataService extends DataService { ); } + public getMappedCollections(itemId: string): Observable> { + const request$ = this.getMappingCollectionsEndpoint(itemId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new MappingCollectionsRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService) + ); + + // TODO: Create a remotedata object + return undefined; + } + } diff --git a/src/app/core/data/mapping-collections-reponse-parsing.service.ts b/src/app/core/data/mapping-collections-reponse-parsing.service.ts new file mode 100644 index 0000000000..1b1ff3368f --- /dev/null +++ b/src/app/core/data/mapping-collections-reponse-parsing.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; + +@Injectable() +export class MappingCollectionsReponseParsingService implements ResponseParsingService { + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + + if (payload._embedded && payload._embedded.mappingCollections) { + const mappingCollections = payload._embedded.mappingCollections; + return new GenericSuccessResponse(mappingCollections, data.statusCode); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from mappingCollections endpoint'), + { statusText: data.statusCode } + ) + ); + } + } +} diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index b87f9cefc8..001c416f69 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -13,6 +13,7 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service'; +import { MappingCollectionsReponseParsingService } from './mapping-collections-reponse-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -191,6 +192,12 @@ export class BrowseItemsRequest extends GetRequest { } } +export class MappingCollectionsRequest extends GetRequest { + getResponseParser(): GenericConstructor { + return MappingCollectionsReponseParsingService; + } +} + export class ConfigRequest extends GetRequest { constructor(uuid: string, href: string) { super(uuid, href); From 7021527f5c579cc1f07d7cf6e00651d0b93faad5 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 16:33:33 +0200 Subject: [PATCH 23/78] 55946: Fixed api calls for fetching collections with/without item --- .../item-collection-mapper.component.ts | 4 +++- .../item-status/item-status.component.ts | 1 - src/app/core/data/item-data.service.ts | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index eeb602292d..1ecbd7d1f0 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -82,7 +82,9 @@ export class ItemCollectionMapperComponent implements OnInit { map((itemRD: RemoteData) => itemRD.payload), switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); - this.mappingCollectionsRD$ = this.collectionDataService.findAll(); + this.mappingCollectionsRD$ = this.searchOptions$.pipe( + switchMap((searchOptions: PaginatedSearchOptions) => this.collectionDataService.findAll(searchOptions)) + ); } /** 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 a7f7e7915a..06dd838ce2 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 @@ -58,7 +58,6 @@ export class ItemStatusComponent implements OnInit { The value is supposed to be a href for the button */ this.actions = Object.assign({ - // TODO: Create mapping component on item level mappedCollections: this.getCurrentUrl() + '/mapper' }); this.actionsKeys = Object.keys(this.actions); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 250d8c1303..0fe9a690e2 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -89,8 +89,17 @@ export class ItemDataService extends DataService { configureRequest(this.requestService) ); - // TODO: Create a remotedata object - return undefined; + const href$ = request$.pipe(map((request: RestRequest) => request.href)); + const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); + const payload$ = responseCache$.pipe( + filterSuccessfulResponses(), + map((entry: ResponseCacheEntry) => entry.response), + map((response: GenericSuccessResponse) => response.payload), + ensureArrayHasValue() + ); + + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } } From c4203f25d517d27fd758c6444a582de7a3b11735 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 17:16:43 +0200 Subject: [PATCH 24/78] 55946: Refactored item-select to object-select to allow for easier implementation of collection-select --- src/app/app.reducer.ts | 6 +- src/app/core/core.module.ts | 4 +- .../shared/item-select/item-select.actions.ts | 75 ----------- .../item-select/item-select.reducer.spec.ts | 98 --------------- .../item-select/item-select.service.spec.ts | 96 -------------- .../shared/item-select/item-select.service.ts | 119 ------------------ .../item-select/item-select.component.html | 0 .../item-select/item-select.component.scss | 0 .../item-select/item-select.component.spec.ts | 37 +++--- .../item-select/item-select.component.ts | 23 ++-- .../object-select/object-select.actions.ts | 75 +++++++++++ .../object-select.reducer.spec.ts | 98 +++++++++++++++ .../object-select.reducer.ts} | 24 ++-- .../object-select.service.spec.ts | 96 ++++++++++++++ .../object-select/object-select.service.ts | 119 ++++++++++++++++++ src/app/shared/shared.module.ts | 2 +- ...-stub.ts => object-select-service-stub.ts} | 2 +- 17 files changed, 434 insertions(+), 440 deletions(-) delete mode 100644 src/app/shared/item-select/item-select.actions.ts delete mode 100644 src/app/shared/item-select/item-select.reducer.spec.ts delete mode 100644 src/app/shared/item-select/item-select.service.spec.ts delete mode 100644 src/app/shared/item-select/item-select.service.ts rename src/app/shared/{ => object-select}/item-select/item-select.component.html (100%) rename src/app/shared/{ => object-select}/item-select/item-select.component.scss (100%) rename src/app/shared/{ => object-select}/item-select/item-select.component.spec.ts (76%) rename src/app/shared/{ => object-select}/item-select/item-select.component.ts (72%) create mode 100644 src/app/shared/object-select/object-select.actions.ts create mode 100644 src/app/shared/object-select/object-select.reducer.spec.ts rename src/app/shared/{item-select/item-select.reducer.ts => object-select/object-select.reducer.ts} (67%) create mode 100644 src/app/shared/object-select/object-select.service.spec.ts create mode 100644 src/app/shared/object-select/object-select.service.ts rename src/app/shared/testing/{item-select-service-stub.ts => object-select-service-stub.ts} (94%) diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index ba882b50b8..4702bbe354 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -14,7 +14,7 @@ import { } from './+search-page/search-filters/search-filter/search-filter.reducer'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; -import { itemSelectionReducer, ItemSelectionsState } from './shared/item-select/item-select.reducer'; +import { objectSelectionReducer, ObjectSelectionsState } from './shared/object-select/object-select.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -25,7 +25,7 @@ export interface AppState { searchSidebar: SearchSidebarState; searchFilter: SearchFiltersState; truncatable: TruncatablesState, - itemSelection: ItemSelectionsState + objectSelection: ObjectSelectionsState } export const appReducers: ActionReducerMap = { @@ -37,7 +37,7 @@ export const appReducers: ActionReducerMap = { searchSidebar: sidebarReducer, searchFilter: filterReducer, truncatable: truncatableReducer, - itemSelection: itemSelectionReducer + objectSelection: objectSelectionReducer }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4ea3aa8df7..256d58b5b8 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -64,8 +64,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { ItemSelectService } from '../shared/item-select/item-select.service'; import { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service'; +import { ObjectSelectService } from '../shared/object-select/object-select.service'; const IMPORTS = [ CommonModule, @@ -131,7 +131,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, - ItemSelectService, + ObjectSelectService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/shared/item-select/item-select.actions.ts b/src/app/shared/item-select/item-select.actions.ts deleted file mode 100644 index 0f17575a28..0000000000 --- a/src/app/shared/item-select/item-select.actions.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { type } from '../ngrx/type'; -import { Action } from '@ngrx/store'; - -export const ItemSelectionActionTypes = { - INITIAL_DESELECT: type('dspace/item-select/INITIAL_DESELECT'), - INITIAL_SELECT: type('dspace/item-select/INITIAL_SELECT'), - SELECT: type('dspace/item-select/SELECT'), - DESELECT: type('dspace/item-select/DESELECT'), - SWITCH: type('dspace/item-select/SWITCH'), - RESET: type('dspace/item-select/RESET') -}; - -export class ItemSelectionAction implements Action { - /** - * UUID of the item a select action can be performed on - */ - id: string; - - /** - * Type of action that will be performed - */ - type; - - /** - * Initialize with the item's UUID - * @param {string} id of the item - */ - constructor(id: string) { - this.id = id; - } -} - -/* tslint:disable:max-classes-per-file */ -/** - * Used to set the initial state to deselected - */ -export class ItemSelectionInitialDeselectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.INITIAL_DESELECT; -} - -/** - * Used to set the initial state to selected - */ -export class ItemSelectionInitialSelectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.INITIAL_SELECT; -} - -/** - * Used to select an item - */ -export class ItemSelectionSelectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.SELECT; -} - -/** - * Used to deselect an item - */ -export class ItemSelectionDeselectAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.DESELECT; -} - -/** - * Used to switch an item between selected and deselected - */ -export class ItemSelectionSwitchAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.SWITCH; -} - -/** - * Used to reset all item's selected to be deselected - */ -export class ItemSelectionResetAction extends ItemSelectionAction { - type = ItemSelectionActionTypes.RESET; -} -/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/item-select/item-select.reducer.spec.ts b/src/app/shared/item-select/item-select.reducer.spec.ts deleted file mode 100644 index 10c5db9b7f..0000000000 --- a/src/app/shared/item-select/item-select.reducer.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ItemSelectionDeselectAction, ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; -import { itemSelectionReducer } from './item-select.reducer'; - -const itemId1 = 'id1'; -const itemId2 = 'id2'; - -class NullAction extends ItemSelectionSelectAction { - type = null; - - constructor() { - super(undefined); - } -} - -describe('itemSelectionReducer', () => { - - it('should return the current state when no valid actions have been made', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new NullAction(); - const newState = itemSelectionReducer(state, action); - - expect(newState).toEqual(state); - }); - - it('should start with an empty object', () => { - const state = {}; - const action = new NullAction(); - const newState = itemSelectionReducer(undefined, action); - - expect(newState).toEqual(state); - }); - - it('should set checked to true in response to the INITIAL_SELECT action', () => { - const action = new ItemSelectionInitialSelectAction(itemId1); - const newState = itemSelectionReducer(undefined, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked to true in response to the INITIAL_DESELECT action', () => { - const action = new ItemSelectionInitialDeselectAction(itemId1); - const newState = itemSelectionReducer(undefined, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set checked to true in response to the SELECT action', () => { - const state = {}; - state[itemId1] = { checked: false }; - const action = new ItemSelectionSelectAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked to false in response to the DESELECT action', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new ItemSelectionDeselectAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set checked from false to true in response to the SWITCH action', () => { - const state = {}; - state[itemId1] = { checked: false }; - const action = new ItemSelectionSwitchAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeTruthy(); - }); - - it('should set checked from true to false in response to the SWITCH action', () => { - const state = {}; - state[itemId1] = { checked: true }; - const action = new ItemSelectionSwitchAction(itemId1); - const newState = itemSelectionReducer(state, action); - - expect(newState[itemId1].checked).toBeFalsy(); - }); - - it('should set reset the state in response to the RESET action', () => { - const state = {}; - state[itemId1] = { checked: true }; - state[itemId2] = { checked: false }; - const action = new ItemSelectionResetAction(undefined); - const newState = itemSelectionReducer(state, action); - - expect(newState).toEqual({}); - }); - -}); diff --git a/src/app/shared/item-select/item-select.service.spec.ts b/src/app/shared/item-select/item-select.service.spec.ts deleted file mode 100644 index f7b28a5b04..0000000000 --- a/src/app/shared/item-select/item-select.service.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ItemSelectService } from './item-select.service'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; -import { ItemSelectionsState } from './item-select.reducer'; -import { AppState } from '../../app.reducer'; -import { - ItemSelectionDeselectAction, - ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; - -describe('ItemSelectService', () => { - let service: ItemSelectService; - - const mockItemId = 'id1'; - - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of(true) - }); - - const appStore: Store = jasmine.createSpyObj('appStore', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of(true) - }); - - beforeEach(() => { - service = new ItemSelectService(store, appStore); - }); - - describe('when the initialSelect method is triggered', () => { - beforeEach(() => { - service.initialSelect(mockItemId); - }); - - it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialSelectAction(mockItemId)); - }); - }); - - describe('when the initialDeselect method is triggered', () => { - beforeEach(() => { - service.initialDeselect(mockItemId); - }); - - it('ItemSelectionInitialDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionInitialDeselectAction(mockItemId)); - }); - }); - - describe('when the select method is triggered', () => { - beforeEach(() => { - service.select(mockItemId); - }); - - it('ItemSelectionSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSelectAction(mockItemId)); - }); - }); - - describe('when the deselect method is triggered', () => { - beforeEach(() => { - service.deselect(mockItemId); - }); - - it('ItemSelectionDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionDeselectAction(mockItemId)); - }); - }); - - describe('when the switch method is triggered', () => { - beforeEach(() => { - service.switch(mockItemId); - }); - - it('ItemSelectionSwitchAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionSwitchAction(mockItemId)); - }); - }); - - describe('when the reset method is triggered', () => { - beforeEach(() => { - service.reset(); - }); - - it('ItemSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ItemSelectionResetAction(null)); - }); - }); - -}); diff --git a/src/app/shared/item-select/item-select.service.ts b/src/app/shared/item-select/item-select.service.ts deleted file mode 100644 index bf4c7ba239..0000000000 --- a/src/app/shared/item-select/item-select.service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; -import { ItemSelectionsState, ItemSelectionState } from './item-select.reducer'; -import { - ItemSelectionDeselectAction, - ItemSelectionInitialDeselectAction, - ItemSelectionInitialSelectAction, ItemSelectionResetAction, - ItemSelectionSelectAction, ItemSelectionSwitchAction -} from './item-select.actions'; -import { Observable } from 'rxjs/Observable'; -import { hasValue } from '../empty.util'; -import { map } from 'rxjs/operators'; -import { AppState } from '../../app.reducer'; - -const selectionStateSelector = (state: ItemSelectionsState) => state.itemSelection; -const itemSelectionsStateSelector = (state: AppState) => state.itemSelection; - -/** - * Service that takes care of selecting and deselecting items - */ -@Injectable() -export class ItemSelectService { - - constructor( - private store: Store, - private appStore: Store - ) { - } - - /** - * Request the current selection of a given item - * @param {string} id The UUID of the item - * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false - */ - getSelected(id: string): Observable { - return this.store.select(selectionByIdSelector(id)).pipe( - map((object: ItemSelectionState) => { - if (object) { - return object.checked; - } else { - return false; - } - }) - ); - } - - /** - * Request the current selection of a given item - * @param {string} id The UUID of the item - * @returns {Observable} Emits the current selection state of the given item, if it's unavailable, return false - */ - getAllSelected(): Observable { - return this.appStore.select(itemSelectionsStateSelector).pipe( - map((state: ItemSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) - ); - } - - /** - * Dispatches an initial select action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public initialSelect(id: string): void { - this.store.dispatch(new ItemSelectionInitialSelectAction(id)); - } - - /** - * Dispatches an initial deselect action to the store for a given item - * @param {string} id The UUID of the item to deselect - */ - public initialDeselect(id: string): void { - this.store.dispatch(new ItemSelectionInitialDeselectAction(id)); - } - - /** - * Dispatches a select action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public select(id: string): void { - this.store.dispatch(new ItemSelectionSelectAction(id)); - } - - /** - * Dispatches a deselect action to the store for a given item - * @param {string} id The UUID of the item to deselect - */ - public deselect(id: string): void { - this.store.dispatch(new ItemSelectionDeselectAction(id)); - } - - /** - * Dispatches a switch action to the store for a given item - * @param {string} id The UUID of the item to select - */ - public switch(id: string): void { - this.store.dispatch(new ItemSelectionSwitchAction(id)); - } - - /** - * Dispatches a reset action to the store for all items - */ - public reset(): void { - this.store.dispatch(new ItemSelectionResetAction(null)); - } - -} - -function selectionByIdSelector(id: string): MemoizedSelector { - return keySelector(id); -} - -export function keySelector(key: string): MemoizedSelector { - return createSelector(selectionStateSelector, (state: ItemSelectionState) => { - if (hasValue(state)) { - return state[key]; - } else { - return undefined; - } - }); -} diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html similarity index 100% rename from src/app/shared/item-select/item-select.component.html rename to src/app/shared/object-select/item-select/item-select.component.html diff --git a/src/app/shared/item-select/item-select.component.scss b/src/app/shared/object-select/item-select/item-select.component.scss similarity index 100% rename from src/app/shared/item-select/item-select.component.scss rename to src/app/shared/object-select/item-select/item-select.component.scss diff --git a/src/app/shared/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts similarity index 76% rename from src/app/shared/item-select/item-select.component.spec.ts rename to src/app/shared/object-select/item-select/item-select.component.spec.ts index 0f3a9d5fae..9708e43ca9 100644 --- a/src/app/shared/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -1,30 +1,25 @@ import { ItemSelectComponent } from './item-select.component'; import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { SharedModule } from '../shared.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { ItemSelectService } from './item-select.service'; -import { ItemSelectServiceStub } from '../testing/item-select-service-stub'; -import { Observable } from 'rxjs/Observable'; -import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { Item } from '../../core/shared/item.model'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute, Route, Router } from '@angular/router'; -import { ActivatedRouteStub } from '../testing/active-router-stub'; -import { RouterStub } from '../testing/router-stub'; -import { HostWindowService } from '../host-window.service'; -import { HostWindowServiceStub } from '../testing/host-window-service-stub'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { LocationStrategy } from '@angular/common'; -import { MockLocationStrategy } from '@angular/common/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { Item } from '../../../core/shared/item.model'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../shared.module'; +import { ObjectSelectServiceStub } from '../../testing/object-select-service-stub'; +import { ObjectSelectService } from '../object-select.service'; +import { HostWindowService } from '../../host-window.service'; +import { HostWindowServiceStub } from '../../testing/host-window-service-stub'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; - let itemSelectService: ItemSelectService; + let itemSelectService: ObjectSelectService; const mockItemList = [ Object.assign(new Item(), { @@ -70,7 +65,7 @@ describe('ItemSelectComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ - { provide: ItemSelectService, useValue: new ItemSelectServiceStub() }, + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts similarity index 72% rename from src/app/shared/item-select/item-select.component.ts rename to src/app/shared/object-select/item-select/item-select.component.ts index 3a2003a327..6ada45cb3b 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -1,12 +1,11 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { ItemDataService } from '../../core/data/item-data.service'; -import { PaginatedList } from '../../core/data/paginated-list'; -import { RemoteData } from '../../core/data/remote-data'; -import { Observable } from 'rxjs/Observable'; -import { Item } from '../../core/shared/item.model'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { ItemSelectService } from './item-select.service'; import { take } from 'rxjs/operators'; +import { Observable } from 'rxjs/Observable'; +import { Item } from '../../../core/shared/item.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { ObjectSelectService } from '../object-select.service'; @Component({ selector: 'ds-item-select', @@ -50,11 +49,11 @@ export class ItemSelectComponent implements OnInit { */ selectedIds$: Observable; - constructor(private itemSelectService: ItemSelectService) { + constructor(private objectelectService: ObjectSelectService) { } ngOnInit(): void { - this.selectedIds$ = this.itemSelectService.getAllSelected(); + this.selectedIds$ = this.objectelectService.getAllSelected(); } /** @@ -62,7 +61,7 @@ export class ItemSelectComponent implements OnInit { * @param {string} id */ switch(id: string) { - this.itemSelectService.switch(id); + this.objectelectService.switch(id); } /** @@ -71,7 +70,7 @@ export class ItemSelectComponent implements OnInit { * @returns {Observable} */ getSelected(id: string): Observable { - return this.itemSelectService.getSelected(id); + return this.objectelectService.getSelected(id); } /** @@ -83,7 +82,7 @@ export class ItemSelectComponent implements OnInit { take(1) ).subscribe((ids: string[]) => { this.confirm.emit(ids); - this.itemSelectService.reset(); + this.objectelectService.reset(); }); } diff --git a/src/app/shared/object-select/object-select.actions.ts b/src/app/shared/object-select/object-select.actions.ts new file mode 100644 index 0000000000..4adaeb9fed --- /dev/null +++ b/src/app/shared/object-select/object-select.actions.ts @@ -0,0 +1,75 @@ +import { type } from '../ngrx/type'; +import { Action } from '@ngrx/store'; + +export const ObjectSelectionActionTypes = { + INITIAL_DESELECT: type('dspace/object-select/INITIAL_DESELECT'), + INITIAL_SELECT: type('dspace/object-select/INITIAL_SELECT'), + SELECT: type('dspace/object-select/SELECT'), + DESELECT: type('dspace/object-select/DESELECT'), + SWITCH: type('dspace/object-select/SWITCH'), + RESET: type('dspace/object-select/RESET') +}; + +export class ObjectSelectionAction implements Action { + /** + * UUID of the object a select action can be performed on + */ + id: string; + + /** + * Type of action that will be performed + */ + type; + + /** + * Initialize with the object's UUID + * @param {string} id of the object + */ + constructor(id: string) { + this.id = id; + } +} + +/* tslint:disable:max-classes-per-file */ +/** + * Used to set the initial state to deselected + */ +export class ObjectSelectionInitialDeselectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.INITIAL_DESELECT; +} + +/** + * Used to set the initial state to selected + */ +export class ObjectSelectionInitialSelectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.INITIAL_SELECT; +} + +/** + * Used to select an object + */ +export class ObjectSelectionSelectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.SELECT; +} + +/** + * Used to deselect an object + */ +export class ObjectSelectionDeselectAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.DESELECT; +} + +/** + * Used to switch an object between selected and deselected + */ +export class ObjectSelectionSwitchAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.SWITCH; +} + +/** + * Used to reset all objects selected to be deselected + */ +export class ObjectSelectionResetAction extends ObjectSelectionAction { + type = ObjectSelectionActionTypes.RESET; +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/object-select/object-select.reducer.spec.ts b/src/app/shared/object-select/object-select.reducer.spec.ts new file mode 100644 index 0000000000..696df97d39 --- /dev/null +++ b/src/app/shared/object-select/object-select.reducer.spec.ts @@ -0,0 +1,98 @@ +import { + ObjectSelectionDeselectAction, ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; +import { objectSelectionReducer } from './object-select.reducer'; + +const objectId1 = 'id1'; +const objectId2 = 'id2'; + +class NullAction extends ObjectSelectionSelectAction { + type = null; + + constructor() { + super(undefined); + } +} + +describe('objectSelectionReducer', () => { + + it('should return the current state when no valid actions have been made', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new NullAction(); + const newState = objectSelectionReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an empty object', () => { + const state = {}; + const action = new NullAction(); + const newState = objectSelectionReducer(undefined, action); + + expect(newState).toEqual(state); + }); + + it('should set checked to true in response to the INITIAL_SELECT action', () => { + const action = new ObjectSelectionInitialSelectAction(objectId1); + const newState = objectSelectionReducer(undefined, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked to true in response to the INITIAL_DESELECT action', () => { + const action = new ObjectSelectionInitialDeselectAction(objectId1); + const newState = objectSelectionReducer(undefined, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set checked to true in response to the SELECT action', () => { + const state = {}; + state[objectId1] = { checked: false }; + const action = new ObjectSelectionSelectAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked to false in response to the DESELECT action', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new ObjectSelectionDeselectAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set checked from false to true in response to the SWITCH action', () => { + const state = {}; + state[objectId1] = { checked: false }; + const action = new ObjectSelectionSwitchAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeTruthy(); + }); + + it('should set checked from true to false in response to the SWITCH action', () => { + const state = {}; + state[objectId1] = { checked: true }; + const action = new ObjectSelectionSwitchAction(objectId1); + const newState = objectSelectionReducer(state, action); + + expect(newState[objectId1].checked).toBeFalsy(); + }); + + it('should set reset the state in response to the RESET action', () => { + const state = {}; + state[objectId1] = { checked: true }; + state[objectId2] = { checked: false }; + const action = new ObjectSelectionResetAction(undefined); + const newState = objectSelectionReducer(state, action); + + expect(newState).toEqual({}); + }); + +}); diff --git a/src/app/shared/item-select/item-select.reducer.ts b/src/app/shared/object-select/object-select.reducer.ts similarity index 67% rename from src/app/shared/item-select/item-select.reducer.ts rename to src/app/shared/object-select/object-select.reducer.ts index 6306adf0c4..bd54e43a35 100644 --- a/src/app/shared/item-select/item-select.reducer.ts +++ b/src/app/shared/object-select/object-select.reducer.ts @@ -1,21 +1,21 @@ import { isEmpty } from '../empty.util'; -import { ItemSelectionAction, ItemSelectionActionTypes } from './item-select.actions'; +import { ObjectSelectionAction, ObjectSelectionActionTypes } from './object-select.actions'; /** * Interface that represents the state for a single filters */ -export interface ItemSelectionState { +export interface ObjectSelectionState { checked: boolean; } /** * Interface that represents the state for all available filters */ -export interface ItemSelectionsState { - [id: string]: ItemSelectionState +export interface ObjectSelectionsState { + [id: string]: ObjectSelectionState } -const initialState: ItemSelectionsState = Object.create(null); +const initialState: ObjectSelectionsState = Object.create(null); /** * Performs a search filter action on the current state @@ -23,11 +23,11 @@ const initialState: ItemSelectionsState = Object.create(null); * @param {SearchFilterAction} action The action that should be performed * @returns {SearchFiltersState} The state after the action is performed */ -export function itemSelectionReducer(state = initialState, action: ItemSelectionAction): ItemSelectionsState { +export function objectSelectionReducer(state = initialState, action: ObjectSelectionAction): ObjectSelectionsState { switch (action.type) { - case ItemSelectionActionTypes.INITIAL_SELECT: { + case ObjectSelectionActionTypes.INITIAL_SELECT: { if (isEmpty(state) || isEmpty(state[action.id])) { return Object.assign({}, state, { [action.id]: { @@ -38,7 +38,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection return state; } - case ItemSelectionActionTypes.INITIAL_DESELECT: { + case ObjectSelectionActionTypes.INITIAL_DESELECT: { if (isEmpty(state) || isEmpty(state[action.id])) { return Object.assign({}, state, { [action.id]: { @@ -49,7 +49,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection return state; } - case ItemSelectionActionTypes.SELECT: { + case ObjectSelectionActionTypes.SELECT: { return Object.assign({}, state, { [action.id]: { checked: true @@ -57,7 +57,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.DESELECT: { + case ObjectSelectionActionTypes.DESELECT: { return Object.assign({}, state, { [action.id]: { checked: false @@ -65,7 +65,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.SWITCH: { + case ObjectSelectionActionTypes.SWITCH: { return Object.assign({}, state, { [action.id]: { checked: (isEmpty(state) || isEmpty(state[action.id])) ? true : !state[action.id].checked @@ -73,7 +73,7 @@ export function itemSelectionReducer(state = initialState, action: ItemSelection }); } - case ItemSelectionActionTypes.RESET: { + case ObjectSelectionActionTypes.RESET: { return {}; } diff --git a/src/app/shared/object-select/object-select.service.spec.ts b/src/app/shared/object-select/object-select.service.spec.ts new file mode 100644 index 0000000000..3b5bcec06f --- /dev/null +++ b/src/app/shared/object-select/object-select.service.spec.ts @@ -0,0 +1,96 @@ +import { ObjectSelectService } from './object-select.service'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { ObjectSelectionsState } from './object-select.reducer'; +import { AppState } from '../../app.reducer'; +import { + ObjectSelectionDeselectAction, + ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; + +describe('ObjectSelectService', () => { + let service: ObjectSelectService; + + const mockObjectId = 'id1'; + + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + const appStore: Store = jasmine.createSpyObj('appStore', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of(true) + }); + + beforeEach(() => { + service = new ObjectSelectService(store, appStore); + }); + + describe('when the initialSelect method is triggered', () => { + beforeEach(() => { + service.initialSelect(mockObjectId); + }); + + it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialSelectAction(mockObjectId)); + }); + }); + + describe('when the initialDeselect method is triggered', () => { + beforeEach(() => { + service.initialDeselect(mockObjectId); + }); + + it('ObjectSelectionInitialDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialDeselectAction(mockObjectId)); + }); + }); + + describe('when the select method is triggered', () => { + beforeEach(() => { + service.select(mockObjectId); + }); + + it('ObjectSelectionSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSelectAction(mockObjectId)); + }); + }); + + describe('when the deselect method is triggered', () => { + beforeEach(() => { + service.deselect(mockObjectId); + }); + + it('ObjectSelectionDeselectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionDeselectAction(mockObjectId)); + }); + }); + + describe('when the switch method is triggered', () => { + beforeEach(() => { + service.switch(mockObjectId); + }); + + it('ObjectSelectionSwitchAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSwitchAction(mockObjectId)); + }); + }); + + describe('when the reset method is triggered', () => { + beforeEach(() => { + service.reset(); + }); + + it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionResetAction(null)); + }); + }); + +}); diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts new file mode 100644 index 0000000000..adc394d4e1 --- /dev/null +++ b/src/app/shared/object-select/object-select.service.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@angular/core'; +import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { ObjectSelectionsState, ObjectSelectionState } from './object-select.reducer'; +import { + ObjectSelectionDeselectAction, + ObjectSelectionInitialDeselectAction, + ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, + ObjectSelectionSelectAction, ObjectSelectionSwitchAction +} from './object-select.actions'; +import { Observable } from 'rxjs/Observable'; +import { hasValue } from '../empty.util'; +import { map } from 'rxjs/operators'; +import { AppState } from '../../app.reducer'; + +const selectionStateSelector = (state: ObjectSelectionsState) => state.objectSelection; +const objectSelectionsStateSelector = (state: AppState) => state.objectSelection; + +/** + * Service that takes care of selecting and deselecting objects + */ +@Injectable() +export class ObjectSelectService { + + constructor( + private store: Store, + private appStore: Store + ) { + } + + /** + * Request the current selection of a given object + * @param {string} id The UUID of the object + * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false + */ + getSelected(id: string): Observable { + return this.store.select(selectionByIdSelector(id)).pipe( + map((object: ObjectSelectionState) => { + if (object) { + return object.checked; + } else { + return false; + } + }) + ); + } + + /** + * Request the current selection of a given object + * @param {string} id The UUID of the object + * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false + */ + getAllSelected(): Observable { + return this.appStore.select(objectSelectionsStateSelector).pipe( + map((state: ObjectSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) + ); + } + + /** + * Dispatches an initial select action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public initialSelect(id: string): void { + this.store.dispatch(new ObjectSelectionInitialSelectAction(id)); + } + + /** + * Dispatches an initial deselect action to the store for a given object + * @param {string} id The UUID of the object to deselect + */ + public initialDeselect(id: string): void { + this.store.dispatch(new ObjectSelectionInitialDeselectAction(id)); + } + + /** + * Dispatches a select action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public select(id: string): void { + this.store.dispatch(new ObjectSelectionSelectAction(id)); + } + + /** + * Dispatches a deselect action to the store for a given object + * @param {string} id The UUID of the object to deselect + */ + public deselect(id: string): void { + this.store.dispatch(new ObjectSelectionDeselectAction(id)); + } + + /** + * Dispatches a switch action to the store for a given object + * @param {string} id The UUID of the object to select + */ + public switch(id: string): void { + this.store.dispatch(new ObjectSelectionSwitchAction(id)); + } + + /** + * Dispatches a reset action to the store for all objects + */ + public reset(): void { + this.store.dispatch(new ObjectSelectionResetAction(null)); + } + +} + +function selectionByIdSelector(id: string): MemoizedSelector { + return keySelector(id); +} + +export function keySelector(key: string): MemoizedSelector { + return createSelector(selectionStateSelector, (state: ObjectSelectionState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c5c6cad09b..a10855bf5e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -83,7 +83,7 @@ import { InputSuggestionsComponent } from './input-suggestions/input-suggestions import { CapitalizePipe } from './utils/capitalize.pipe'; import { MomentModule } from 'angular2-moment'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; -import { ItemSelectComponent } from './item-select/item-select.component'; +import { ItemSelectComponent } from './object-select/item-select/item-select.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here diff --git a/src/app/shared/testing/item-select-service-stub.ts b/src/app/shared/testing/object-select-service-stub.ts similarity index 94% rename from src/app/shared/testing/item-select-service-stub.ts rename to src/app/shared/testing/object-select-service-stub.ts index 690d1e1435..f4bcccae77 100644 --- a/src/app/shared/testing/item-select-service-stub.ts +++ b/src/app/shared/testing/object-select-service-stub.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs/Observable'; -export class ItemSelectServiceStub { +export class ObjectSelectServiceStub { ids: string[] = []; From 0b3b5d3965aaca7a6ca69a9659ac281f1f0e2841 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 10 Oct 2018 13:30:29 +0200 Subject: [PATCH 25/78] 55946: Refactoring of object-select and collection-select component --- resources/i18n/en.json | 6 + .../collection-item-mapper.component.html | 2 +- .../item-collection-mapper.component.html | 8 +- .../item-collection-mapper.component.ts | 10 ++ .../collection-select.component.html | 27 ++++ .../collection-select.component.scss | 0 .../collection-select.component.spec.ts | 126 ++++++++++++++++++ .../collection-select.component.ts | 29 ++++ .../item-select/item-select.component.html | 38 +++--- .../item-select/item-select.component.ts | 73 ++-------- .../object-select/object-select.component.ts | 86 ++++++++++++ src/app/shared/shared.module.ts | 4 +- 12 files changed, 323 insertions(+), 86 deletions(-) create mode 100644 src/app/shared/object-select/collection-select/collection-select.component.html create mode 100644 src/app/shared/object-select/collection-select/collection-select.component.scss create mode 100644 src/app/shared/object-select/collection-select/collection-select.component.spec.ts create mode 100644 src/app/shared/object-select/collection-select/collection-select.component.ts create mode 100644 src/app/shared/object-select/object-select/object-select.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 2db47f300c..05d7e300b0 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -34,6 +34,12 @@ } }, "return": "Return" + }, + "select": { + "table": { + "title": "Title" + }, + "confirm": "Confirm selected" } }, "community": { diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index a7a1416cb0..7ba2d8d68a 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -32,7 +32,7 @@
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 21149eafaf..990ac70c64 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -15,7 +15,7 @@
- +
@@ -26,7 +26,11 @@
- +
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 1ecbd7d1f0..6f80fcde27 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -87,6 +87,16 @@ export class ItemCollectionMapperComponent implements OnInit { ); } + /** + * Map the item to the selected collections and display notifications + * @param {string[]} ids The list of collection UUID's to map the item to + */ + mapCollections(ids: string[]) { + // TODO: Map item to selected collections and display notifications + console.log('mapped to collections:'); + console.log(ids); + } + /** * Clear url parameters on tab change (temporary fix until pagination is improved) * @param event diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html new file mode 100644 index 0000000000..551d33ba3b --- /dev/null +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -0,0 +1,27 @@ + + +
+ + + + + + + + + + + + + +
{{'collection.select.table.title' | translate}}
{{collection.name}}
+
+
+ +
diff --git a/src/app/shared/object-select/collection-select/collection-select.component.scss b/src/app/shared/object-select/collection-select/collection-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts new file mode 100644 index 0000000000..ae4a6aa0a7 --- /dev/null +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -0,0 +1,126 @@ +import { CollectionSelectComponent } from './item-select.component'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Item } from '../../../core/shared/item.model'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../shared.module'; +import { ObjectSelectServiceStub } from '../../testing/object-select-service-stub'; +import { ObjectSelectService } from '../object-select.service'; +import { HostWindowService } from '../../host-window.service'; +import { HostWindowServiceStub } from '../../testing/host-window-service-stub'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +describe('ItemSelectComponent', () => { + let comp: CollectionSelectComponent; + let fixture: ComponentFixture; + let itemSelectService: ObjectSelectService; + + const mockItemList = [ + Object.assign(new Item(), { + id: 'id1', + bitstreams: Observable.of({}), + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'This is just a title' + }, + { + key: 'dc.type', + language: null, + value: 'Article' + }] + }), + Object.assign(new Item(), { + id: 'id2', + bitstreams: Observable.of({}), + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'This is just another title' + }, + { + key: 'dc.type', + language: null, + value: 'Article' + }] + }) + ]; + const mockItems = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); + const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-page-configuration', + pageSize: 10, + currentPage: 1 + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], + declarations: [], + providers: [ + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionSelectComponent); + comp = fixture.componentInstance; + comp.itemsRD$ = mockItems; + comp.paginationOptions = mockPaginationOptions; + fixture.detectChanges(); + itemSelectService = (comp as any).itemSelectService; + }); + + it(`should show a list of ${mockItemList.length} items`, () => { + const tbody: HTMLElement = fixture.debugElement.query(By.css('table#item-select tbody')).nativeElement; + expect(tbody.children.length).toBe(mockItemList.length); + }); + + describe('checkboxes', () => { + let checkbox: HTMLInputElement; + + beforeEach(() => { + checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement; + }); + + it('should initially be unchecked',() => { + expect(checkbox.checked).toBeFalsy(); + }); + + it('should be checked when clicked', () => { + checkbox.click(); + fixture.detectChanges(); + expect(checkbox.checked).toBeTruthy(); + }); + + it('should switch the value through item-select-service', () => { + spyOn((comp as any).itemSelectService, 'switch').and.callThrough(); + checkbox.click(); + expect((comp as any).itemSelectService.switch).toHaveBeenCalled(); + }); + }); + + describe('when confirm is clicked', () => { + let confirmButton: HTMLButtonElement; + + beforeEach(() => { + confirmButton = fixture.debugElement.query(By.css('button.item-confirm')).nativeElement; + spyOn(comp.confirm, 'emit').and.callThrough(); + }); + + it('should emit the selected items',() => { + confirmButton.click(); + expect(comp.confirm.emit).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/shared/object-select/collection-select/collection-select.component.ts b/src/app/shared/object-select/collection-select/collection-select.component.ts new file mode 100644 index 0000000000..489e9109fc --- /dev/null +++ b/src/app/shared/object-select/collection-select/collection-select.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { Collection } from '../../../core/shared/collection.model'; +import { ObjectSelectComponent } from '../object-select/object-select.component'; +import { isNotEmpty } from '../../empty.util'; +import { ObjectSelectService } from '../object-select.service'; + +@Component({ + selector: 'ds-collection-select', + styleUrls: ['./collection-select.component.scss'], + templateUrl: './collection-select.component.html' +}) + +/** + * A component used to select collections from a specific list and returning the UUIDs of the selected collections + */ +export class CollectionSelectComponent extends ObjectSelectComponent { + + constructor(protected objectSelectService: ObjectSelectService) { + super(objectSelectService); + } + + ngOnInit(): void { + super.ngOnInit(); + if (!isNotEmpty(this.confirmButton)) { + this.confirmButton = 'collection.select.confirm'; + } + } + +} diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 9c08cfae87..9546623ecf 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -1,29 +1,31 @@ - -
- - + + +
+
+ - - - + + + - -
{{'item.select.table.collection' | translate}} {{'item.select.table.author' | translate}} {{'item.select.table.title' | translate}}
{{(item.owningCollection | async)?.payload?.name}} {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}}
-
-
- + + +
+ + + diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index 6ada45cb3b..348be4b37d 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -6,6 +6,8 @@ import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; import { ObjectSelectService } from '../object-select.service'; +import { ObjectSelectComponent } from '../object-select/object-select.component'; +import { isNotEmpty } from '../../empty.util'; @Component({ selector: 'ds-item-select', @@ -16,74 +18,17 @@ import { ObjectSelectService } from '../object-select.service'; /** * A component used to select items from a specific list and returning the UUIDs of the selected items */ -export class ItemSelectComponent implements OnInit { +export class ItemSelectComponent extends ObjectSelectComponent { - /** - * The list of items to display - */ - @Input() - itemsRD$: Observable>>; - - /** - * The pagination options used to display the items - */ - @Input() - paginationOptions: PaginationComponentOptions; - - /** - * The message key used for the confirm button - * @type {string} - */ - @Input() - confirmButton = 'item.select.confirm'; - - /** - * EventEmitter to return the selected UUIDs when the confirm button is pressed - * @type {EventEmitter} - */ - @Output() - confirm: EventEmitter = new EventEmitter(); - - /** - * The list of selected UUIDs - */ - selectedIds$: Observable; - - constructor(private objectelectService: ObjectSelectService) { + constructor(protected objectSelectService: ObjectSelectService) { + super(objectSelectService); } ngOnInit(): void { - this.selectedIds$ = this.objectelectService.getAllSelected(); - } - - /** - * Switch the state of a checkbox - * @param {string} id - */ - switch(id: string) { - this.objectelectService.switch(id); - } - - /** - * Get the current state of a checkbox - * @param {string} id The item's UUID - * @returns {Observable} - */ - getSelected(id: string): Observable { - return this.objectelectService.getSelected(id); - } - - /** - * Called when the confirm button is pressed - * Sends the selected UUIDs to the parent component - */ - confirmSelected() { - this.selectedIds$.pipe( - take(1) - ).subscribe((ids: string[]) => { - this.confirm.emit(ids); - this.objectelectService.reset(); - }); + super.ngOnInit(); + if (!isNotEmpty(this.confirmButton)) { + this.confirmButton = 'item.select.confirm'; + } } } diff --git a/src/app/shared/object-select/object-select/object-select.component.ts b/src/app/shared/object-select/object-select/object-select.component.ts new file mode 100644 index 0000000000..fb9e28edc2 --- /dev/null +++ b/src/app/shared/object-select/object-select/object-select.component.ts @@ -0,0 +1,86 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; +import { ObjectSelectService } from '../object-select.service'; + +/** + * An abstract component used to select DSpaceObjects from a specific list and returning the UUIDs of the selected DSpaceObjects + */ +export abstract class ObjectSelectComponent implements OnInit, OnDestroy { + + /** + * The list of DSpaceObjects to display + */ + @Input() + dsoRD$: Observable>>; + + /** + * The pagination options used to display the DSpaceObjects + */ + @Input() + paginationOptions: PaginationComponentOptions; + + /** + * The message key used for the confirm button + * @type {string} + */ + @Input() + confirmButton: string; + + /** + * EventEmitter to return the selected UUIDs when the confirm button is pressed + * @type {EventEmitter} + */ + @Output() + confirm: EventEmitter = new EventEmitter(); + + /** + * The list of selected UUIDs + */ + selectedIds$: Observable; + + constructor(protected objectSelectService: ObjectSelectService) { + } + + ngOnInit(): void { + this.selectedIds$ = this.objectSelectService.getAllSelected(); + } + + ngOnDestroy(): void { + this.objectSelectService.reset(); + } + + /** + * Switch the state of a checkbox + * @param {string} id + */ + switch(id: string) { + this.objectSelectService.switch(id); + } + + /** + * Get the current state of a checkbox + * @param {string} id The dso's UUID + * @returns {Observable} + */ + getSelected(id: string): Observable { + return this.objectSelectService.getSelected(id); + } + + /** + * Called when the confirm button is pressed + * Sends the selected UUIDs to the parent component + */ + confirmSelected() { + this.selectedIds$.pipe( + take(1) + ).subscribe((ids: string[]) => { + this.confirm.emit(ids); + this.objectSelectService.reset(); + }); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a10855bf5e..6cb9098410 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -84,6 +84,7 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { MomentModule } from 'angular2-moment'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { ItemSelectComponent } from './object-select/item-select/item-select.component'; +import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -158,7 +159,8 @@ const COMPONENTS = [ TruncatablePartComponent, BrowseByComponent, InputSuggestionsComponent, - ItemSelectComponent + ItemSelectComponent, + CollectionSelectComponent ]; const ENTRY_COMPONENTS = [ From 0cf91fb05e7d3809f8bc3126ed82252ea53bb2b2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 10 Oct 2018 13:51:02 +0200 Subject: [PATCH 26/78] 55946: Removal of unnecessary scss files and test fixes --- .../collection-select.component.scss | 0 .../collection-select.component.spec.ts | 64 ++++++------------- .../collection-select.component.ts | 1 - .../item-select/item-select.component.scss | 0 .../item-select/item-select.component.spec.ts | 12 ++-- .../item-select/item-select.component.ts | 1 - 6 files changed, 27 insertions(+), 51 deletions(-) delete mode 100644 src/app/shared/object-select/collection-select/collection-select.component.scss delete mode 100644 src/app/shared/object-select/item-select/item-select.component.scss diff --git a/src/app/shared/object-select/collection-select/collection-select.component.scss b/src/app/shared/object-select/collection-select/collection-select.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index ae4a6aa0a7..477f823928 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -1,7 +1,5 @@ -import { CollectionSelectComponent } from './item-select.component'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { Item } from '../../../core/shared/item.model'; import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; @@ -15,45 +13,25 @@ import { HostWindowService } from '../../host-window.service'; import { HostWindowServiceStub } from '../../testing/host-window-service-stub'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { CollectionSelectComponent } from './collection-select.component'; +import { Collection } from '../../../core/shared/collection.model'; describe('ItemSelectComponent', () => { let comp: CollectionSelectComponent; let fixture: ComponentFixture; - let itemSelectService: ObjectSelectService; + let objectSelectService: ObjectSelectService; - const mockItemList = [ - Object.assign(new Item(), { + const mockCollectionList = [ + Object.assign(new Collection(), { id: 'id1', - bitstreams: Observable.of({}), - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'This is just a title' - }, - { - key: 'dc.type', - language: null, - value: 'Article' - }] + name: 'name1' }), - Object.assign(new Item(), { + Object.assign(new Collection(), { id: 'id2', - bitstreams: Observable.of({}), - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'This is just another title' - }, - { - key: 'dc.type', - language: null, - value: 'Article' - }] + name: 'name2' }) ]; - const mockItems = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); + const mockCollections = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockCollectionList))); const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, @@ -75,22 +53,22 @@ describe('ItemSelectComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CollectionSelectComponent); comp = fixture.componentInstance; - comp.itemsRD$ = mockItems; + comp.dsoRD$ = mockCollections; comp.paginationOptions = mockPaginationOptions; fixture.detectChanges(); - itemSelectService = (comp as any).itemSelectService; + objectSelectService = (comp as any).objectSelectService; }); - it(`should show a list of ${mockItemList.length} items`, () => { - const tbody: HTMLElement = fixture.debugElement.query(By.css('table#item-select tbody')).nativeElement; - expect(tbody.children.length).toBe(mockItemList.length); + it(`should show a list of ${mockCollectionList.length} collections`, () => { + const tbody: HTMLElement = fixture.debugElement.query(By.css('table#collection-select tbody')).nativeElement; + expect(tbody.children.length).toBe(mockCollectionList.length); }); describe('checkboxes', () => { let checkbox: HTMLInputElement; beforeEach(() => { - checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement; + checkbox = fixture.debugElement.query(By.css('input.collection-checkbox')).nativeElement; }); it('should initially be unchecked',() => { @@ -103,10 +81,10 @@ describe('ItemSelectComponent', () => { expect(checkbox.checked).toBeTruthy(); }); - it('should switch the value through item-select-service', () => { - spyOn((comp as any).itemSelectService, 'switch').and.callThrough(); + it('should switch the value through object-select-service', () => { + spyOn((comp as any).objectSelectService, 'switch').and.callThrough(); checkbox.click(); - expect((comp as any).itemSelectService.switch).toHaveBeenCalled(); + expect((comp as any).objectSelectService.switch).toHaveBeenCalled(); }); }); @@ -114,11 +92,11 @@ describe('ItemSelectComponent', () => { let confirmButton: HTMLButtonElement; beforeEach(() => { - confirmButton = fixture.debugElement.query(By.css('button.item-confirm')).nativeElement; + confirmButton = fixture.debugElement.query(By.css('button.collection-confirm')).nativeElement; spyOn(comp.confirm, 'emit').and.callThrough(); }); - it('should emit the selected items',() => { + it('should emit the selected collections',() => { confirmButton.click(); expect(comp.confirm.emit).toHaveBeenCalled(); }); diff --git a/src/app/shared/object-select/collection-select/collection-select.component.ts b/src/app/shared/object-select/collection-select/collection-select.component.ts index 489e9109fc..3d40b469da 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.ts @@ -6,7 +6,6 @@ import { ObjectSelectService } from '../object-select.service'; @Component({ selector: 'ds-collection-select', - styleUrls: ['./collection-select.component.scss'], templateUrl: './collection-select.component.html' }) diff --git a/src/app/shared/object-select/item-select/item-select.component.scss b/src/app/shared/object-select/item-select/item-select.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index 9708e43ca9..e07858360e 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -19,7 +19,7 @@ import { By } from '@angular/platform-browser'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; - let itemSelectService: ObjectSelectService; + let objectSelectService: ObjectSelectService; const mockItemList = [ Object.assign(new Item(), { @@ -75,10 +75,10 @@ describe('ItemSelectComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ItemSelectComponent); comp = fixture.componentInstance; - comp.itemsRD$ = mockItems; + comp.dsoRD$ = mockItems; comp.paginationOptions = mockPaginationOptions; fixture.detectChanges(); - itemSelectService = (comp as any).itemSelectService; + objectSelectService = (comp as any).objectSelectService; }); it(`should show a list of ${mockItemList.length} items`, () => { @@ -103,10 +103,10 @@ describe('ItemSelectComponent', () => { expect(checkbox.checked).toBeTruthy(); }); - it('should switch the value through item-select-service', () => { - spyOn((comp as any).itemSelectService, 'switch').and.callThrough(); + it('should switch the value through object-select-service', () => { + spyOn((comp as any).objectSelectService, 'switch').and.callThrough(); checkbox.click(); - expect((comp as any).itemSelectService.switch).toHaveBeenCalled(); + expect((comp as any).objectSelectService.switch).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index 348be4b37d..d8d4eef34a 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -11,7 +11,6 @@ import { isNotEmpty } from '../../empty.util'; @Component({ selector: 'ds-item-select', - styleUrls: ['./item-select.component.scss'], templateUrl: './item-select.component.html' }) From dd38e612306c2f7c3e177f2b807344047bc0ad39 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 10 Oct 2018 15:31:58 +0200 Subject: [PATCH 27/78] 55946: Functional item-collection-mapper --- .../item-collection-mapper.component.ts | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 6f80fcde27..8708065488 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -15,6 +15,9 @@ import { SearchConfigurationService } from '../../../+search-page/search-service import { map, switchMap } from 'rxjs/operators'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; +import { RestResponse } from '../../../core/cache/response-cache.models'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; @Component({ selector: 'ds-item-collection-mapper', @@ -52,18 +55,14 @@ export class ItemCollectionMapperComponent implements OnInit { */ mappingCollectionsRD$: Observable>>; - /** - * Sort on title ASC by default - * @type {SortOptions} - */ - defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); - constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, private searchService: SearchService, private collectionDataService: CollectionDataService, - private itemDataService: ItemDataService) { + private notificationsService: NotificationsService, + private itemDataService: ItemDataService, + private translateService: TranslateService) { } ngOnInit(): void { @@ -92,9 +91,36 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {string[]} ids The list of collection UUID's to map the item to */ mapCollections(ids: string[]) { - // TODO: Map item to selected collections and display notifications - console.log('mapped to collections:'); - console.log(ids); + const responses$ = this.itemRD$.pipe( + getSucceededRemoteData(), + map((itemRD: RemoteData) => itemRD.payload.id), + switchMap((itemId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) + ); + + responses$.subscribe((responses: RestResponse[]) => { + const successful = responses.filter((response: RestResponse) => response.isSuccessful); + const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); + if (successful.length > 0) { + const successMessages = Observable.combineLatest( + this.translateService.get('item.edit.item-mapper.notifications.success.head'), + this.translateService.get('item.edit.item-mapper.notifications.success.content', { amount: successful.length }) + ); + + successMessages.subscribe(([head, content]) => { + this.notificationsService.success(head, content); + }); + } + if (unsuccessful.length > 0) { + const unsuccessMessages = Observable.combineLatest( + this.translateService.get('item.edit.item-mapper.notifications.error.head'), + this.translateService.get('item.edit.item-mapper.notifications.error.content', { amount: unsuccessful.length }) + ); + + unsuccessMessages.subscribe(([head, content]) => { + this.notificationsService.error(head, content); + }); + } + }); } /** From 378fbe86f4473648b1d3d7801f9a3732c5291f12 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 10 Oct 2018 16:15:53 +0200 Subject: [PATCH 28/78] 55946: Functionality for removing mappings --- resources/i18n/en.json | 29 ++++++++++++---- .../item-collection-mapper.component.html | 8 +++-- .../item-collection-mapper.component.ts | 34 ++++++++++++++++--- src/app/core/data/item-data.service.ts | 26 +++++++++++--- ...ing-collections-reponse-parsing.service.ts | 12 ++++++- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 05d7e300b0..dcc19e6a21 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -135,19 +135,34 @@ "head": "Item Mapper - Map Item to Collections", "item": "Item: \"{{name}}\"", "description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - "confirm": "Map item to selected collections", "tabs": { "browse": "Browse", "map": "Map" }, + "buttons": { + "add": "Map item to selected collections", + "remove": "Remove item's mapping for selected collections" + }, "notifications": { - "success": { - "head": "Mapping completed", - "content": "Successfully mapped item to {{amount}} collections." + "add": { + "success": { + "head": "Mapping completed", + "content": "Successfully mapped item to {{amount}} collections." + }, + "error": { + "head": "Mapping errors", + "content": "Errors occurred for mapping of item to {{amount}} collections." + } }, - "error": { - "head": "Mapping errors", - "content": "Errors occurred for mapping of item to {{amount}} collections." + "remove": { + "success": { + "head": "Removal of mapping completed", + "content": "Successfully removed mapping of item to {{amount}} collections." + }, + "error": { + "head": "Removal of mapping errors", + "content": "Errors occurred for the removal of the mapping to {{amount}} collections." + } } }, "return": "Return" diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 990ac70c64..f02a62b769 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -19,7 +19,11 @@
-
{{col.name}}
+
@@ -29,7 +33,7 @@
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 8708065488..95c2ffd361 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -18,6 +18,7 @@ import { ItemDataService } from '../../../core/data/item-data.service'; import { RestResponse } from '../../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { C } from '@angular/core/src/render3'; @Component({ selector: 'ds-item-collection-mapper', @@ -47,7 +48,7 @@ export class ItemCollectionMapperComponent implements OnInit { * List of collections to show under the "Browse" tab * Collections that are mapped to the item */ - itemCollectionsRD$: Observable>; + itemCollectionsRD$: Observable>>; /** * List of collections to show under the "Map" tab @@ -97,13 +98,36 @@ export class ItemCollectionMapperComponent implements OnInit { switchMap((itemId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) ); + this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); + } + + /** + * Remove the mapping of the item to the selected collections and display notifications + * @param {string[]} ids The list of collection UUID's to remove the mapping of the item for + */ + removeMappings(ids: string[]) { + const responses$ = this.itemRD$.pipe( + getSucceededRemoteData(), + map((itemRD: RemoteData) => itemRD.payload.id), + switchMap((itemId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id)))) + ); + + this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); + } + + /** + * Display notifications + * @param {Observable} responses$ The responses after adding/removing a mapping + * @param {string} messagePrefix The prefix to build the notification messages with + */ + private showNotifications(responses$: Observable, messagePrefix: string) { responses$.subscribe((responses: RestResponse[]) => { const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { const successMessages = Observable.combineLatest( - this.translateService.get('item.edit.item-mapper.notifications.success.head'), - this.translateService.get('item.edit.item-mapper.notifications.success.content', { amount: successful.length }) + this.translateService.get(`${messagePrefix}.success.head`), + this.translateService.get(`${messagePrefix}.success.content`, { amount: successful.length }) ); successMessages.subscribe(([head, content]) => { @@ -112,8 +136,8 @@ export class ItemCollectionMapperComponent implements OnInit { } if (unsuccessful.length > 0) { const unsuccessMessages = Observable.combineLatest( - this.translateService.get('item.edit.item-mapper.notifications.error.head'), - this.translateService.get('item.edit.item-mapper.notifications.error.content', { amount: unsuccessful.length }) + this.translateService.get(`${messagePrefix}.error.head`), + this.translateService.get(`${messagePrefix}.error.content`, { amount: unsuccessful.length }) ); unsuccessMessages.subscribe(([head, content]) => { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 0fe9a690e2..3b6d3d90ab 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -15,7 +15,14 @@ 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, GetRequest, MappingCollectionsRequest, PostRequest, RestRequest } from './request.models'; +import { + DeleteRequest, + FindAllOptions, + GetRequest, + MappingCollectionsRequest, + PostRequest, + RestRequest +} from './request.models'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { configureRequest, @@ -69,6 +76,18 @@ export class ItemDataService extends DataService { ); } + public removeMappingFromCollection(itemId: string, collectionId: string): Observable { + return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService), + map((request: RestRequest) => request.href), + getResponseFromSelflink(this.responseCache), + map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response) + ); + } + public mapToCollection(itemId: string, collectionId: string): Observable { return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), @@ -81,7 +100,7 @@ export class ItemDataService extends DataService { ); } - public getMappedCollections(itemId: string): Observable> { + public getMappedCollections(itemId: string): Observable>> { const request$ = this.getMappingCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), @@ -95,8 +114,7 @@ export class ItemDataService extends DataService { const payload$ = responseCache$.pipe( filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), - map((response: GenericSuccessResponse) => response.payload), - ensureArrayHasValue() + map((response: GenericSuccessResponse>) => response.payload) ); return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); diff --git a/src/app/core/data/mapping-collections-reponse-parsing.service.ts b/src/app/core/data/mapping-collections-reponse-parsing.service.ts index 1b1ff3368f..0ae014301c 100644 --- a/src/app/core/data/mapping-collections-reponse-parsing.service.ts +++ b/src/app/core/data/mapping-collections-reponse-parsing.service.ts @@ -3,6 +3,8 @@ import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; @Injectable() export class MappingCollectionsReponseParsingService implements ResponseParsingService { @@ -11,7 +13,15 @@ export class MappingCollectionsReponseParsingService implements ResponseParsingS if (payload._embedded && payload._embedded.mappingCollections) { const mappingCollections = payload._embedded.mappingCollections; - return new GenericSuccessResponse(mappingCollections, data.statusCode); + // TODO: When the API supports it, change this to fetch a paginated list, instead of creating static one + // Reason: Pagination is currently not supported on the mappingCollections endpoint + const paginatedMappingCollections = new PaginatedList(Object.assign(new PageInfo(), { + elementsPerPage: mappingCollections.length, + totalElements: mappingCollections.length, + totalPages: 1, + currentPage: 1 + }), mappingCollections); + return new GenericSuccessResponse(paginatedMappingCollections, data.statusCode); } else { return new ErrorResponse( Object.assign( From 497f089c2fe689b653d0603b70ccc63b526c6628 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 10 Oct 2018 17:31:18 +0200 Subject: [PATCH 29/78] 55946: Improvement on mapping by excluding already existing mapped collections --- .../item-collection-mapper.component.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 95c2ffd361..ce82c99c8a 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -92,10 +92,22 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {string[]} ids The list of collection UUID's to map the item to */ mapCollections(ids: string[]) { - const responses$ = this.itemRD$.pipe( - getSucceededRemoteData(), - map((itemRD: RemoteData) => itemRD.payload.id), - switchMap((itemId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) + const itemIdAndExcludingIds$ = Observable.combineLatest( + this.itemRD$.pipe( + getSucceededRemoteData(), + map((rd: RemoteData) => rd.payload), + map((item: Item) => item.id) + ), + this.itemCollectionsRD$.pipe( + getSucceededRemoteData(), + map((rd: RemoteData>) => rd.payload.page), + map((collections: Collection[]) => collections.map((collection: Collection) => collection.id)) + ) + ); + + // Map the item to the collections found in ids, excluding the collections the item is already mapped to + const responses$ = itemIdAndExcludingIds$.pipe( + switchMap(([itemId, excludingIds]) => Observable.combineLatest(this.filterIds(ids, excludingIds).map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); @@ -106,6 +118,7 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {string[]} ids The list of collection UUID's to remove the mapping of the item for */ removeMappings(ids: string[]) { + // TODO: When the API supports fetching collections excluding the item's scope, make sure to exclude ids from mappingCollectionsRD$ here const responses$ = this.itemRD$.pipe( getSucceededRemoteData(), map((itemRD: RemoteData) => itemRD.payload.id), @@ -115,6 +128,16 @@ export class ItemCollectionMapperComponent implements OnInit { this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); } + /** + * Filters ids from a given list of ids, which exist in a second given list of ids + * @param {string[]} ids The list of ids to filter out of + * @param {string[]} excluding The ids that should be excluded from the first list + * @returns {string[]} + */ + private filterIds(ids: string[], excluding: string[]): string[] { + return ids.filter((id: string) => excluding.indexOf(id) < 0); + } + /** * Display notifications * @param {Observable} responses$ The responses after adding/removing a mapping From 8f0d7b6a4ec1cbff2a0ecb2c8a51ec5e763ca115 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 Oct 2018 10:10:45 +0200 Subject: [PATCH 30/78] 55946: ItemCollectionMapperComponent tests --- .../item-collection-mapper.component.spec.ts | 162 ++++++++++++++++++ .../item-collection-mapper.component.ts | 1 - 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index e69de29bb2..1eb91e30b1 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -0,0 +1,162 @@ +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonModule } from '@angular/common'; +import { ItemCollectionMapperComponent } from './item-collection-mapper.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; +import { SearchService } from '../../../+search-page/search-service/search.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Observable } from 'rxjs/Observable'; +import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { RestResponse } from '../../../core/cache/response-cache.models'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; +import { EventEmitter } from '@angular/core'; +import { SearchServiceStub } from '../../../shared/testing/search-service-stub'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../../shared/shared.module'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { HostWindowService } from '../../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub'; +import { By } from '@angular/platform-browser'; +import { Item } from '../../../core/shared/item.model'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ObjectSelectService } from '../../../shared/object-select/object-select.service'; +import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub'; + +fdescribe('ItemCollectionMapperComponent', () => { + let comp: ItemCollectionMapperComponent; + let fixture: ComponentFixture; + + let route: ActivatedRoute; + let router: Router; + let searchConfigService: SearchConfigurationService; + let notificationsService: NotificationsService; + let itemDataService: ItemDataService; + let collectionDataService: CollectionDataService; + + const mockItem: Item = Object.assign(new Item(), { + id: '932c7d50-d85a-44cb-b9dc-b427b12877bd', + name: 'test-item' + }); + const mockItemRD: RemoteData = new RemoteData(false, false, true, null, mockItem); + const mockSearchOptions = Observable.of(new PaginatedSearchOptions({ + pagination: Object.assign(new PaginationComponentOptions(), { + id: 'search-page-configuration', + pageSize: 10, + currentPage: 1 + }), + sort: new SortOptions('dc.title', SortDirection.ASC) + })); + const routerStub = Object.assign(new RouterStub(), { + url: 'http://test.url' + }); + const searchConfigServiceStub = { + paginatedSearchOptions: mockSearchOptions + }; + const mockCollectionsRD = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); + const itemDataServiceStub = { + mapToCollection: () => Observable.of(new RestResponse(true, '200')), + removeMappingFromCollection: () => Observable.of(new RestResponse(true, '200')), + getMappedCollections: () => Observable.of(mockCollectionsRD) + }; + const collectionDataServiceStub = { + findAll: () => Observable.of(mockCollectionsRD) + }; + const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD }); + const translateServiceStub = { + get: () => Observable.of('test-message of item ' + mockItem.name), + onLangChange: new EventEmitter(), + onTranslationChange: new EventEmitter(), + onDefaultLangChange: new EventEmitter() + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [ItemCollectionMapperComponent], + providers: [ + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: Router, useValue: routerStub }, + { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: ItemDataService, useValue: itemDataServiceStub }, + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, + { provide: TranslateService, useValue: translateServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemCollectionMapperComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + route = (comp as any).route; + router = (comp as any).router; + searchConfigService = (comp as any).searchConfigService; + notificationsService = (comp as any).notificationsService; + itemDataService = (comp as any).itemDataService; + collectionDataService = (comp as any).collectionDataService; + }); + + it('should display the correct collection name', () => { + const name: HTMLElement = fixture.debugElement.query(By.css('#item-name')).nativeElement; + expect(name.innerHTML).toContain(mockItem.name); + }); + + describe('mapCollections', () => { + const ids = ['id1', 'id2', 'id3', 'id4']; + + beforeEach(() => { + spyOn(notificationsService, 'success').and.callThrough(); + spyOn(notificationsService, 'error').and.callThrough(); + }); + + it('should display a success message if at least one mapping was successful', () => { + comp.mapCollections(ids); + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + }); + + it('should display an error message if at least one mapping was unsuccessful', () => { + spyOn(itemDataService, 'mapToCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + comp.mapCollections(ids); + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + + describe('removeMappings', () => { + const ids = ['id1', 'id2', 'id3', 'id4']; + + beforeEach(() => { + spyOn(notificationsService, 'success').and.callThrough(); + spyOn(notificationsService, 'error').and.callThrough(); + }); + + it('should display a success message if the removal of at least one mapping was successful', () => { + comp.removeMappings(ids); + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + }); + + it('should display an error message if the removal of at least one mapping was unsuccessful', () => { + spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + comp.removeMappings(ids); + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + +}); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index ce82c99c8a..03d42596ab 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -59,7 +59,6 @@ export class ItemCollectionMapperComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, - private searchService: SearchService, private collectionDataService: CollectionDataService, private notificationsService: NotificationsService, private itemDataService: ItemDataService, From fc967004759c2460de6ba2d5f76de0cf0d2076a2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 Oct 2018 11:16:47 +0200 Subject: [PATCH 31/78] 55946: Fixed search query not working --- .../item-collection-mapper.component.html | 1 - .../item-collection-mapper.component.spec.ts | 14 +++++++------- .../item-collection-mapper.component.ts | 14 ++++++++++---- src/app/core/data/data.service.ts | 4 ++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index f02a62b769..c1989acf22 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -9,7 +9,6 @@
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 1eb91e30b1..fcd9a18d49 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -33,16 +33,16 @@ import { CollectionDataService } from '../../../core/data/collection-data.servic import { ObjectSelectService } from '../../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub'; -fdescribe('ItemCollectionMapperComponent', () => { +describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; let fixture: ComponentFixture; let route: ActivatedRoute; let router: Router; let searchConfigService: SearchConfigurationService; + let searchService: SearchService; let notificationsService: NotificationsService; let itemDataService: ItemDataService; - let collectionDataService: CollectionDataService; const mockItem: Item = Object.assign(new Item(), { id: '932c7d50-d85a-44cb-b9dc-b427b12877bd', @@ -69,9 +69,9 @@ fdescribe('ItemCollectionMapperComponent', () => { removeMappingFromCollection: () => Observable.of(new RestResponse(true, '200')), getMappedCollections: () => Observable.of(mockCollectionsRD) }; - const collectionDataServiceStub = { - findAll: () => Observable.of(mockCollectionsRD) - }; + const searchServiceStub = Object.assign(new SearchServiceStub(), { + search: () => Observable.of(mockCollectionsRD) + }); const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD }); const translateServiceStub = { get: () => Observable.of('test-message of item ' + mockItem.name), @@ -90,7 +90,7 @@ fdescribe('ItemCollectionMapperComponent', () => { { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: ItemDataService, useValue: itemDataServiceStub }, - { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: SearchService, useValue: searchServiceStub }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: TranslateService, useValue: translateServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } @@ -107,7 +107,7 @@ fdescribe('ItemCollectionMapperComponent', () => { searchConfigService = (comp as any).searchConfigService; notificationsService = (comp as any).notificationsService; itemDataService = (comp as any).itemDataService; - collectionDataService = (comp as any).collectionDataService; + searchService = (comp as any).searchService; }); it('should display the correct collection name', () => { diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 03d42596ab..45755a52c0 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -8,7 +8,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; @@ -19,6 +19,7 @@ import { RestResponse } from '../../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { C } from '@angular/core/src/render3'; +import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; @Component({ selector: 'ds-item-collection-mapper', @@ -59,7 +60,7 @@ export class ItemCollectionMapperComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, - private collectionDataService: CollectionDataService, + private searchService: SearchService, private notificationsService: NotificationsService, private itemDataService: ItemDataService, private translateService: TranslateService) { @@ -82,8 +83,13 @@ export class ItemCollectionMapperComponent implements OnInit { switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); this.mappingCollectionsRD$ = this.searchOptions$.pipe( - switchMap((searchOptions: PaginatedSearchOptions) => this.collectionDataService.findAll(searchOptions)) - ); + switchMap((searchOptions: PaginatedSearchOptions) => { + return this.searchService.search(Object.assign(searchOptions, { + dsoType: DSpaceObjectType.COLLECTION + })); + }), + toDSpaceObjectListRD() + ) as Observable>>; } /** diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 3948c3672f..3c22ccfad9 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,4 +1,4 @@ -import { filter, take } from 'rxjs/operators'; +import { filter, take, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -58,7 +58,7 @@ export abstract class DataService hrefObs.pipe( filter((href: string) => hasValue(href)), - take(1)) + take(1), tap((value) => console.log(value))) .subscribe((href: string) => { const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); this.requestService.configure(request); From 3d9e4a66ff8b18ef31684fba5a6273ad5729c9f8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 6 Nov 2018 16:18:25 +0100 Subject: [PATCH 32/78] 55693: Mapped items using new REST endpoint + item-select component --- resources/i18n/en.json | 1 + .../collection-item-mapper.component.html | 12 ++-- .../collection-item-mapper.component.ts | 20 +++++-- src/app/core/core.module.ts | 2 + src/app/core/data/collection-data.service.ts | 59 +++++++++++++++++++ .../mapping-items-response-parsing.service.ts | 44 ++++++++++++++ .../item-select/item-select.component.html | 4 +- .../item-select/item-select.component.ts | 3 + .../object-collection.component.ts | 1 + 9 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 src/app/core/data/mapping-items-response-parsing.service.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 86ff55da66..0c6ce010d8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -19,6 +19,7 @@ "collection": "Collection: \"{{name}}\"", "description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", "confirm": "Map selected items", + "remove": "Remove selected item mappings", "tabs": { "browse": "Browse", "map": "Map" diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index a7a1416cb0..2a83a4bdd6 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -19,12 +19,12 @@
- - +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 15f7db63b4..24f5b07ae5 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -17,6 +17,8 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { ItemDataService } from '../../core/data/item-data.service'; import { RestResponse } from '../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { Item } from '../../core/shared/item.model'; @Component({ selector: 'ds-collection-item-mapper', @@ -67,6 +69,7 @@ export class CollectionItemMapperComponent implements OnInit { private searchService: SearchService, private notificationsService: NotificationsService, private itemDataService: ItemDataService, + private collectionDataService: CollectionDataService, private translateService: TranslateService) { } @@ -88,13 +91,10 @@ export class CollectionItemMapperComponent implements OnInit { ); this.collectionItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options]) => { - return this.searchService.search(Object.assign(options, { - scope: collectionRD.payload.id, - dsoType: DSpaceObjectType.ITEM, + return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { sort: this.defaultSortOptions - })); - }), - toDSpaceObjectListRD() + })) + }) ); this.mappingItemsRD$ = this.searchOptions$.pipe( flatMap((options: PaginatedSearchOptions) => { @@ -145,6 +145,14 @@ export class CollectionItemMapperComponent implements OnInit { }); } + /** + * Remove the mapping for the selected items to the collection and display notifications + * @param {string[]} ids The list of item UUID's to remove the mapping to the collection + */ + unmapItems(ids: string[]) { + // TODO: Functionality for unmapping items + } + /** * Clear url parameters on tab change (temporary fix until pagination is improved) * @param event diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 31b9b31244..73c55a3df4 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -65,6 +65,7 @@ import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { ItemSelectService } from '../shared/item-select/item-select.service'; +import { MappingItemsResponseParsingService } from './data/mapping-items-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -130,6 +131,7 @@ const PROVIDERS = [ UUIDService, DSpaceObjectDataService, ItemSelectService, + MappingItemsResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 7d1e463dbe..103fdee163 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -11,6 +11,26 @@ import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { Item } from '../shared/item.model'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { ensureArrayHasValue, hasValue, isNotEmptyOperator } from '../../shared/empty.util'; +import { GetRequest, RestRequest } from './request.models'; +import { + configureRequest, + filterSuccessfulResponses, + getRequestFromSelflink, + getResponseFromSelflink +} from '../shared/operators'; +import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { ResponseParsingService } from './parsing.service'; +import { MappingItemsResponseParsingService } from './mapping-items-response-parsing.service'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { GenericSuccessResponse } from '../cache/response-cache.models'; +import { DSpaceObject } from '../shared/dspace-object.model'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -27,4 +47,43 @@ export class CollectionDataService extends ComColDataService { + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getFindByIDHref(endpoint, collectionId)), + map((endpoint: string) => `${endpoint}/mappingItems`) + ); + } + + getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const href$ = this.getMappingItemsEndpoint(collectionId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) + ); + + href$.pipe( + map((endpoint: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), endpoint); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return MappingItemsResponseParsingService; + } + }); + }), + configureRequest(this.requestService) + ).subscribe(); + + const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); + + const payload$ = responseCache$.pipe( + filterSuccessfulResponses(), + map((entry: ResponseCacheEntry) => entry.response), + map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)) + ); + + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + } + } diff --git a/src/app/core/data/mapping-items-response-parsing.service.ts b/src/app/core/data/mapping-items-response-parsing.service.ts new file mode 100644 index 0000000000..5d3c39dece --- /dev/null +++ b/src/app/core/data/mapping-items-response-parsing.service.ts @@ -0,0 +1,44 @@ +import { Inject, Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { NormalizedItem } from '../cache/models/normalized-item.model'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Item } from '../shared/item.model'; + +@Injectable() +export class MappingItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = NormalizedObjectFactory; + protected toCache = true; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded) + && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { + const serializer = new DSpaceRESTv2Serializer(DSpaceObject); + const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload)); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from mappingItems endpoint'), + { statusText: data.statusCode } + ) + ); + } + } + +} diff --git a/src/app/shared/item-select/item-select.component.html b/src/app/shared/item-select/item-select.component.html index 9c08cfae87..8caefb096c 100644 --- a/src/app/shared/item-select/item-select.component.html +++ b/src/app/shared/item-select/item-select.component.html @@ -10,7 +10,7 @@ - {{'item.select.table.collection' | translate}} + {{'item.select.table.collection' | translate}} {{'item.select.table.author' | translate}} {{'item.select.table.title' | translate}} @@ -18,7 +18,7 @@ - {{(item.owningCollection | async)?.payload?.name}} + {{(item.owningCollection | async)?.payload?.name}} {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} {{item.findMetadata("dc.title")}} diff --git a/src/app/shared/item-select/item-select.component.ts b/src/app/shared/item-select/item-select.component.ts index 3a2003a327..68949c2baf 100644 --- a/src/app/shared/item-select/item-select.component.ts +++ b/src/app/shared/item-select/item-select.component.ts @@ -38,6 +38,9 @@ export class ItemSelectComponent implements OnInit { @Input() confirmButton = 'item.select.confirm'; + @Input() + hideCollection = false; + /** * EventEmitter to return the selected UUIDs when the confirm button is pressed * @type {EventEmitter} diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index b4a436c5de..fc4a4e9768 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -75,6 +75,7 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { this.currentMode = params.view; } }); + console.log(this.objects); } /** From dd3691349627997bc06b2430b9abb76abda62408 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 6 Nov 2018 16:38:00 +0100 Subject: [PATCH 33/78] 55693: Functionality for removing mappings --- resources/i18n/en.json | 24 ++++++++++---- .../collection-item-mapper.component.html | 2 +- .../collection-item-mapper.component.ts | 31 +++++++++---------- src/app/core/data/item-data.service.ts | 14 ++++++++- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 0c6ce010d8..3a70a3b6d6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -25,13 +25,25 @@ "map": "Map" }, "notifications": { - "success": { - "head": "Mapping completed", - "content": "Successfully mapped {{amount}} items." + "map": { + "success": { + "head": "Mapping completed", + "content": "Successfully mapped {{amount}} items." + }, + "error": { + "head": "Mapping errors", + "content": "Errors occurred for mapping of {{amount}} items." + } }, - "error": { - "head": "Mapping errors", - "content": "Errors occurred for mapping of {{amount}} items." + "unmap": { + "success": { + "head": "Remove mapping completed", + "content": "Successfully removed the mappings of {{amount}} items." + }, + "error": { + "head": "Remove mapping errors", + "content": "Errors occurred for removing the mappings of {{amount}} items." + } } }, "return": "Return" diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 2a83a4bdd6..2408f2540a 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -24,7 +24,7 @@ [paginationOptions]="(searchOptions$ | async)?.pagination" [confirmButton]="'collection.item-mapper.remove'" [hideCollection]="true" - (confirm)="unmapItems($event)"> + (confirm)="mapItems($event, true)">
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 24f5b07ae5..531b48d258 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -109,23 +109,30 @@ export class CollectionItemMapperComponent implements OnInit { } /** - * Map the selected items to the collection and display notifications - * @param {string[]} ids The list of item UUID's to map to the collection + * Map/Unmap the selected items to the collection and display notifications + * @param ids The list of item UUID's to map/unmap to the collection + * @param remove Whether or not it's supposed to remove mappings */ - mapItems(ids: string[]) { + mapItems(ids: string[], remove?: boolean) { const responses$ = this.collectionRD$.pipe( getSucceededRemoteData(), map((collectionRD: RemoteData) => collectionRD.payload.id), - switchMap((collectionId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.mapToCollection(id, collectionId)))) + switchMap((collectionId: string) => + Observable.combineLatest(ids.map((id: string) => + remove ? this.itemDataService.removeMappingFromCollection(id, collectionId) : this.itemDataService.mapToCollection(id, collectionId) + )) + ) ); + const messageInsertion = remove ? 'unmap' : 'map'; + responses$.subscribe((responses: RestResponse[]) => { const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { const successMessages = Observable.combineLatest( - this.translateService.get('collection.item-mapper.notifications.success.head'), - this.translateService.get('collection.item-mapper.notifications.success.content', { amount: successful.length }) + this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.head`), + this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.content`, { amount: successful.length }) ); successMessages.subscribe(([head, content]) => { @@ -134,8 +141,8 @@ export class CollectionItemMapperComponent implements OnInit { } if (unsuccessful.length > 0) { const unsuccessMessages = Observable.combineLatest( - this.translateService.get('collection.item-mapper.notifications.error.head'), - this.translateService.get('collection.item-mapper.notifications.error.content', { amount: unsuccessful.length }) + this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.head`), + this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.content`, { amount: unsuccessful.length }) ); unsuccessMessages.subscribe(([head, content]) => { @@ -145,14 +152,6 @@ export class CollectionItemMapperComponent implements OnInit { }); } - /** - * Remove the mapping for the selected items to the collection and display notifications - * @param {string[]} ids The list of item UUID's to remove the mapping to the collection - */ - unmapItems(ids: string[]) { - // TODO: Functionality for unmapping items - } - /** * Clear url parameters on tab change (temporary fix until pagination is improved) * @param event diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 7c2c4e572d..2a863dc6a7 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -15,7 +15,7 @@ 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, PostRequest, RestRequest } from './request.models'; +import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { configureRequest, getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; @@ -71,4 +71,16 @@ export class ItemDataService extends DataService { ); } + public removeMappingFromCollection(itemId: string, collectionId: string): Observable { + return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService), + map((request: RestRequest) => request.href), + getResponseFromSelflink(this.responseCache), + map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response) + ); + } + } From 9902209fb94fe5e6cb6758e1696635459eb1e8db Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 7 Nov 2018 13:09:00 +0100 Subject: [PATCH 34/78] 55946: Small import fix --- .../collection-item-mapper.component.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index d7ae41f023..06d108d9a4 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -7,7 +7,6 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SearchFormComponent } from '../../shared/search-form/search-form.component'; import { SearchPageModule } from '../../+search-page/search-page.module'; import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component'; -import { ItemSelectComponent } from '../../shared/item-select/item-select.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouterStub } from '../../shared/testing/router-stub'; From 0d89eb6cce2a366964e486c9b446b48f2448c816 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 7 Nov 2018 14:39:49 +0100 Subject: [PATCH 35/78] 55693: TODO location query --- .../collection-item-mapper.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 531b48d258..88c5579f02 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -96,9 +96,11 @@ export class CollectionItemMapperComponent implements OnInit { })) }) ); - this.mappingItemsRD$ = this.searchOptions$.pipe( - flatMap((options: PaginatedSearchOptions) => { + this.mappingItemsRD$ = collectionAndOptions$.pipe( + switchMap(([collectionRD, options]) => { return this.searchService.search(Object.assign(options, { + // TODO: Exclude items already mapped to collection without overwriting search query + // query: `-location.coll:\"${collectionRD.payload.id}\"`, scope: undefined, dsoType: DSpaceObjectType.ITEM, sort: this.defaultSortOptions From 7bd8fae72a7b547197ce91b5bca0b2211ebba571 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 13 Nov 2018 15:31:32 +0100 Subject: [PATCH 36/78] 55693: Fixed getMappedItems to use object cache instead of response --- src/app/core/core.module.ts | 2 - src/app/core/data/collection-data.service.ts | 30 +++---------- .../mapping-items-response-parsing.service.ts | 44 ------------------- 3 files changed, 7 insertions(+), 69 deletions(-) delete mode 100644 src/app/core/data/mapping-items-response-parsing.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 73c55a3df4..31b9b31244 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -65,7 +65,6 @@ import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { ItemSelectService } from '../shared/item-select/item-select.service'; -import { MappingItemsResponseParsingService } from './data/mapping-items-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -131,7 +130,6 @@ const PROVIDERS = [ UUIDService, DSpaceObjectDataService, ItemSelectService, - MappingItemsResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 103fdee163..b4648097c3 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -14,23 +13,17 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs/Observable'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; -import { Item } from '../shared/item.model'; import { distinctUntilChanged, map } from 'rxjs/operators'; -import { ensureArrayHasValue, hasValue, isNotEmptyOperator } from '../../shared/empty.util'; -import { GetRequest, RestRequest } from './request.models'; +import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; +import { GetRequest } from './request.models'; import { - configureRequest, - filterSuccessfulResponses, - getRequestFromSelflink, - getResponseFromSelflink + configureRequest } from '../shared/operators'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from './parsing.service'; -import { MappingItemsResponseParsingService } from './mapping-items-response-parsing.service'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { GenericSuccessResponse } from '../cache/response-cache.models'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { DSOResponseParsingService } from './dso-response-parsing.service'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -67,23 +60,14 @@ export class CollectionDataService extends ComColDataService { - return MappingItemsResponseParsingService; + return DSOResponseParsingService; } }); }), configureRequest(this.requestService) ).subscribe(); - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - - const payload$ = responseCache$.pipe( - filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => entry.response), - map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)) - ); - - return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdbService.buildList(href$); } } diff --git a/src/app/core/data/mapping-items-response-parsing.service.ts b/src/app/core/data/mapping-items-response-parsing.service.ts deleted file mode 100644 index 5d3c39dece..0000000000 --- a/src/app/core/data/mapping-items-response-parsing.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; -import { isNotEmpty } from '../../shared/empty.util'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { NormalizedItem } from '../cache/models/normalized-item.model'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { Item } from '../shared/item.model'; - -@Injectable() -export class MappingItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected objectFactory = NormalizedObjectFactory; - protected toCache = true; - - constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService, - ) { super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded) - && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceRESTv2Serializer(DSpaceObject); - const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); - return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from mappingItems endpoint'), - { statusText: data.statusCode } - ) - ); - } - } - -} From 469b424cfe87d7359239327b17de2e69ba21f9f4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 13 Nov 2018 17:50:02 +0100 Subject: [PATCH 37/78] 55693: Map tab excludes already mapped items --- .../collection-item-mapper.component.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 88c5579f02..704ec61ee1 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -19,6 +19,7 @@ import { RestResponse } from '../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Item } from '../../core/shared/item.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-collection-item-mapper', @@ -98,9 +99,8 @@ export class CollectionItemMapperComponent implements OnInit { ); this.mappingItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options]) => { - return this.searchService.search(Object.assign(options, { - // TODO: Exclude items already mapped to collection without overwriting search query - // query: `-location.coll:\"${collectionRD.payload.id}\"`, + return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { + query: this.buildQuery(collectionRD.payload.id, options.query), scope: undefined, dsoType: DSpaceObjectType.ITEM, sort: this.defaultSortOptions @@ -175,4 +175,13 @@ export class CollectionItemMapperComponent implements OnInit { return this.router.url; } + buildQuery(collectionId: string, query: string): string { + const excludeColQuery = `-location.coll:\"${collectionId}\"`; + if (isNotEmpty(query)) { + return `${excludeColQuery} AND ${query}`; + } else { + return excludeColQuery; + } + } + } From e61467bb7f7cc1cc3c6a968dc521143dba19b148 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 14 Nov 2018 10:37:27 +0100 Subject: [PATCH 38/78] 55693: JSDocs and Test fixes --- .../collection-item-mapper.component.spec.ts | 19 +++++++++++++++---- .../collection-item-mapper.component.ts | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index d7ae41f023..6aeefcd8c4 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -32,6 +32,11 @@ import { By } from '@angular/platform-browser'; import { RestResponse } from '../../core/cache/response-cache.models'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { ItemSelectService } from '../../shared/item-select/item-select.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; +import { ItemSelectServiceStub } from '../../shared/testing/item-select-service-stub'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -74,14 +79,18 @@ describe('CollectionItemMapperComponent', () => { onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() }; + const emptyList = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); const searchServiceStub = Object.assign(new SearchServiceStub(), { - search: () => Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))) + search: () => Observable.of(emptyList) }); + const collectionDataServiceStub = { + getMappedItems: () => Observable.of(emptyList) + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [CommonModule, FormsModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], - declarations: [CollectionItemMapperComponent], + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: Router, useValue: routerStub }, @@ -89,8 +98,10 @@ describe('CollectionItemMapperComponent', () => { { provide: SearchService, useValue: searchServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: ItemDataService, useValue: itemDataServiceStub }, + { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: TranslateService, useValue: translateServiceStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: ItemSelectService, useValue: new ItemSelectServiceStub() } ] }).compileComponents(); })); diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 704ec61ee1..1bd39f4cc8 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -175,6 +175,11 @@ export class CollectionItemMapperComponent implements OnInit { return this.router.url; } + /** + * Build a query where items that are already mapped to a collection are excluded from + * @param collectionId The collection's UUID + * @param query The query to add to it + */ buildQuery(collectionId: string, query: string): string { const excludeColQuery = `-location.coll:\"${collectionId}\"`; if (isNotEmpty(query)) { From b11a168e72ce04cc3a9f4f975a059d1b151aa2b2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 14 Nov 2018 10:54:47 +0100 Subject: [PATCH 39/78] 55946: CollectionItemMapper test fixes after merge --- .../collection-item-mapper.component.spec.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 0e5738094a..0007a96195 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -24,7 +24,7 @@ import { Observable } from 'rxjs/Observable'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { EventEmitter } from '@angular/core'; +import { EventEmitter, NgModule } from '@angular/core'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; import { By } from '@angular/platform-browser'; @@ -32,10 +32,12 @@ import { RestResponse } from '../../core/cache/response-cache.models'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; -import { ItemSelectService } from '../../shared/item-select/item-select.service'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; -import { ItemSelectServiceStub } from '../../shared/testing/item-select-service-stub'; +import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component'; +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'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -89,7 +91,7 @@ describe('CollectionItemMapperComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], - declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe], + declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: Router, useValue: routerStub }, @@ -100,7 +102,7 @@ describe('CollectionItemMapperComponent', () => { { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: TranslateService, useValue: translateServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: ItemSelectService, useValue: new ItemSelectServiceStub() } + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() } ] }).compileComponents(); })); From 08063154e74ffa71b91c0835714f1a670a75dc8a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 15 Nov 2018 14:13:16 +0100 Subject: [PATCH 40/78] 55946: object-select store lists intermediate commit --- .../object-select/object-select.actions.ts | 9 ++- .../object-select/object-select.reducer.ts | 65 +++++++++++++------ .../object-select/object-select.service.ts | 57 +++++++++------- 3 files changed, 84 insertions(+), 47 deletions(-) diff --git a/src/app/shared/object-select/object-select.actions.ts b/src/app/shared/object-select/object-select.actions.ts index 4adaeb9fed..f6c3e035fa 100644 --- a/src/app/shared/object-select/object-select.actions.ts +++ b/src/app/shared/object-select/object-select.actions.ts @@ -11,6 +11,11 @@ export const ObjectSelectionActionTypes = { }; export class ObjectSelectionAction implements Action { + /** + * Key of the list (of selections) for which the action should be performed + */ + key: string; + /** * UUID of the object a select action can be performed on */ @@ -23,9 +28,11 @@ export class ObjectSelectionAction implements Action { /** * Initialize with the object's UUID + * @param {string} key of the list * @param {string} id of the object */ - constructor(id: string) { + constructor(key: string, id: string) { + this.key = key; this.id = id; } } diff --git a/src/app/shared/object-select/object-select.reducer.ts b/src/app/shared/object-select/object-select.reducer.ts index bd54e43a35..a023ce4a9e 100644 --- a/src/app/shared/object-select/object-select.reducer.ts +++ b/src/app/shared/object-select/object-select.reducer.ts @@ -2,36 +2,45 @@ import { isEmpty } from '../empty.util'; import { ObjectSelectionAction, ObjectSelectionActionTypes } from './object-select.actions'; /** - * Interface that represents the state for a single filters + * Interface that represents the state for a single selection of an object */ export interface ObjectSelectionState { checked: boolean; } /** - * Interface that represents the state for all available filters + * Interface that represents the state for all selected items within a certain category defined by a key */ export interface ObjectSelectionsState { [id: string]: ObjectSelectionState } -const initialState: ObjectSelectionsState = Object.create(null); +/** + * Interface that represents the state for all selected items + */ +export interface ObjectSelectionListState { + [key: string]: ObjectSelectionsState +} + +const initialState: ObjectSelectionListState = Object.create(null); /** - * Performs a search filter action on the current state - * @param {SearchFiltersState} state The state before the action is performed - * @param {SearchFilterAction} action The action that should be performed - * @returns {SearchFiltersState} The state after the action is performed + * Performs a selection action on the current state + * @param {ObjectSelectionListState} state The state before the action is performed + * @param {ObjectSelectionAction} action The action that should be performed + * @returns {ObjectSelectionListState} The state after the action is performed */ -export function objectSelectionReducer(state = initialState, action: ObjectSelectionAction): ObjectSelectionsState { +export function objectSelectionReducer(state = initialState, action: ObjectSelectionAction): ObjectSelectionListState { switch (action.type) { case ObjectSelectionActionTypes.INITIAL_SELECT: { - if (isEmpty(state) || isEmpty(state[action.id])) { + if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) { return Object.assign({}, state, { - [action.id]: { - checked: true + [action.key]: { + [action.id]: { + checked: true + } } }); } @@ -39,10 +48,12 @@ export function objectSelectionReducer(state = initialState, action: ObjectSelec } case ObjectSelectionActionTypes.INITIAL_DESELECT: { - if (isEmpty(state) || isEmpty(state[action.id])) { + if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) { return Object.assign({}, state, { - [action.id]: { - checked: false + [action.key]: { + [action.id]: { + checked: false + } } }); } @@ -51,30 +62,42 @@ export function objectSelectionReducer(state = initialState, action: ObjectSelec case ObjectSelectionActionTypes.SELECT: { return Object.assign({}, state, { - [action.id]: { - checked: true + [action.key]: { + [action.id]: { + checked: true + } } }); } case ObjectSelectionActionTypes.DESELECT: { return Object.assign({}, state, { - [action.id]: { - checked: false + [action.key]: { + [action.id]: { + checked: false + } } }); } case ObjectSelectionActionTypes.SWITCH: { return Object.assign({}, state, { - [action.id]: { - checked: (isEmpty(state) || isEmpty(state[action.id])) ? true : !state[action.id].checked + [action.key]: { + [action.id]: { + checked: (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) ? true : !state[action.key][action.id].checked + } } }); } case ObjectSelectionActionTypes.RESET: { - return {}; + if (isEmpty(action.key)) { + return {}; + } else { + return Object.assign({}, state, { + [action.key]: {} + }); + } } default: { diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts index adc394d4e1..91772d2db4 100644 --- a/src/app/shared/object-select/object-select.service.ts +++ b/src/app/shared/object-select/object-select.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; -import { ObjectSelectionsState, ObjectSelectionState } from './object-select.reducer'; +import { ObjectSelectionListState, ObjectSelectionsState, ObjectSelectionState } from './object-select.reducer'; import { ObjectSelectionDeselectAction, ObjectSelectionInitialDeselectAction, @@ -13,7 +13,8 @@ import { map } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; const selectionStateSelector = (state: ObjectSelectionsState) => state.objectSelection; -const objectSelectionsStateSelector = (state: AppState) => state.objectSelection; +const objectSelectionsStateSelector = (state: ObjectSelectionListState) => state.objectSelection; +const objectSelectionListStateSelector = (state: AppState) => state.objectSelection; /** * Service that takes care of selecting and deselecting objects @@ -28,11 +29,12 @@ export class ObjectSelectService { } /** - * Request the current selection of a given object + * Request the current selection of a given object in a given list + * @param {string} key The key of the list where the selection resides in * @param {string} id The UUID of the object * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false */ - getSelected(id: string): Observable { + getSelected(key: string, id: string): Observable { return this.store.select(selectionByIdSelector(id)).pipe( map((object: ObjectSelectionState) => { if (object) { @@ -45,9 +47,8 @@ export class ObjectSelectService { } /** - * Request the current selection of a given object - * @param {string} id The UUID of the object - * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false + * Request the current selection of all objects + * @returns {Observable} Emits the current selection state of all objects */ getAllSelected(): Observable { return this.appStore.select(objectSelectionsStateSelector).pipe( @@ -56,50 +57,56 @@ export class ObjectSelectService { } /** - * Dispatches an initial select action to the store for a given object + * Dispatches an initial select action to the store for a given object in a given list + * @param {string} key The key of the list to select the object in * @param {string} id The UUID of the object to select */ - public initialSelect(id: string): void { - this.store.dispatch(new ObjectSelectionInitialSelectAction(id)); + public initialSelect(key: string, id: string): void { + this.store.dispatch(new ObjectSelectionInitialSelectAction(key, id)); } /** - * Dispatches an initial deselect action to the store for a given object + * Dispatches an initial deselect action to the store for a given object in a given list + * @param {string} key The key of the list to deselect the object in * @param {string} id The UUID of the object to deselect */ - public initialDeselect(id: string): void { - this.store.dispatch(new ObjectSelectionInitialDeselectAction(id)); + public initialDeselect(key: string, id: string): void { + this.store.dispatch(new ObjectSelectionInitialDeselectAction(key, id)); } /** - * Dispatches a select action to the store for a given object + * Dispatches a select action to the store for a given object in a given list + * @param {string} key The key of the list to select the object in * @param {string} id The UUID of the object to select */ - public select(id: string): void { - this.store.dispatch(new ObjectSelectionSelectAction(id)); + public select(key: string, id: string): void { + this.store.dispatch(new ObjectSelectionSelectAction(key, id)); } /** - * Dispatches a deselect action to the store for a given object + * Dispatches a deselect action to the store for a given object in a given list + * @param {string} key The key of the list to deselect the object in * @param {string} id The UUID of the object to deselect */ - public deselect(id: string): void { - this.store.dispatch(new ObjectSelectionDeselectAction(id)); + public deselect(key: string, id: string): void { + this.store.dispatch(new ObjectSelectionDeselectAction(key, id)); } /** - * Dispatches a switch action to the store for a given object + * Dispatches a switch action to the store for a given object in a given list + * @param {string} key The key of the list to select the object in * @param {string} id The UUID of the object to select */ - public switch(id: string): void { - this.store.dispatch(new ObjectSelectionSwitchAction(id)); + public switch(key: string, id: string): void { + this.store.dispatch(new ObjectSelectionSwitchAction(key, id)); } /** - * Dispatches a reset action to the store for all objects + * Dispatches a reset action to the store for all objects (in a list) + * @param {string} key The key of the list to clear all selections for */ - public reset(): void { - this.store.dispatch(new ObjectSelectionResetAction(null)); + public reset(key?: string): void { + this.store.dispatch(new ObjectSelectionResetAction(key, null)); } } From 8d396f383283312efcec2ba5057689efbdce7cee Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 16 Nov 2018 13:55:00 +0100 Subject: [PATCH 41/78] 55946: Multi-list object select support --- .../collection-item-mapper.component.html | 4 ++- .../item-collection-mapper.component.html | 2 ++ src/app/app.reducer.ts | 8 ++++-- .../object-select/object-select.reducer.ts | 20 +++++++-------- .../object-select/object-select.service.ts | 25 +++++++++---------- .../object-select/object-select.component.ts | 13 ++++++---- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 20da923cc3..b455ecfe3f 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -15,11 +15,12 @@ - +
= { diff --git a/src/app/shared/object-select/object-select.reducer.ts b/src/app/shared/object-select/object-select.reducer.ts index a023ce4a9e..617e0242e0 100644 --- a/src/app/shared/object-select/object-select.reducer.ts +++ b/src/app/shared/object-select/object-select.reducer.ts @@ -37,11 +37,11 @@ export function objectSelectionReducer(state = initialState, action: ObjectSelec case ObjectSelectionActionTypes.INITIAL_SELECT: { if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) { return Object.assign({}, state, { - [action.key]: { + [action.key]: Object.assign({}, state[action.key], { [action.id]: { checked: true } - } + }) }); } return state; @@ -50,11 +50,11 @@ export function objectSelectionReducer(state = initialState, action: ObjectSelec case ObjectSelectionActionTypes.INITIAL_DESELECT: { if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) { return Object.assign({}, state, { - [action.key]: { + [action.key]: Object.assign({}, state[action.key], { [action.id]: { checked: false } - } + }) }); } return state; @@ -62,31 +62,31 @@ export function objectSelectionReducer(state = initialState, action: ObjectSelec case ObjectSelectionActionTypes.SELECT: { return Object.assign({}, state, { - [action.key]: { + [action.key]: Object.assign({}, state[action.key], { [action.id]: { checked: true } - } + }) }); } case ObjectSelectionActionTypes.DESELECT: { return Object.assign({}, state, { - [action.key]: { + [action.key]: Object.assign({}, state[action.key], { [action.id]: { checked: false } - } + }) }); } case ObjectSelectionActionTypes.SWITCH: { return Object.assign({}, state, { - [action.key]: { + [action.key]: Object.assign({}, state[action.key], { [action.id]: { checked: (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) ? true : !state[action.key][action.id].checked } - } + }) }); } diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts index 91772d2db4..2dbb69fe53 100644 --- a/src/app/shared/object-select/object-select.service.ts +++ b/src/app/shared/object-select/object-select.service.ts @@ -12,7 +12,6 @@ import { hasValue } from '../empty.util'; import { map } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; -const selectionStateSelector = (state: ObjectSelectionsState) => state.objectSelection; const objectSelectionsStateSelector = (state: ObjectSelectionListState) => state.objectSelection; const objectSelectionListStateSelector = (state: AppState) => state.objectSelection; @@ -23,7 +22,7 @@ const objectSelectionListStateSelector = (state: AppState) => state.objectSelect export class ObjectSelectService { constructor( - private store: Store, + private store: Store, private appStore: Store ) { } @@ -35,7 +34,7 @@ export class ObjectSelectService { * @returns {Observable} Emits the current selection state of the given object, if it's unavailable, return false */ getSelected(key: string, id: string): Observable { - return this.store.select(selectionByIdSelector(id)).pipe( + return this.store.select(selectionByKeyAndIdSelector(key, id)).pipe( map((object: ObjectSelectionState) => { if (object) { return object.checked; @@ -47,12 +46,12 @@ export class ObjectSelectService { } /** - * Request the current selection of all objects + * Request the current selection of all objects within a specific list * @returns {Observable} Emits the current selection state of all objects */ - getAllSelected(): Observable { - return this.appStore.select(objectSelectionsStateSelector).pipe( - map((state: ObjectSelectionsState) => Object.keys(state).filter((key) => state[key].checked)) + getAllSelected(key: string): Observable { + return this.appStore.select(objectSelectionListStateSelector).pipe( + map((state: ObjectSelectionListState) => Object.keys(state[key]).filter((id) => state[key][id].checked)) ); } @@ -111,14 +110,14 @@ export class ObjectSelectService { } -function selectionByIdSelector(id: string): MemoizedSelector { - return keySelector(id); +function selectionByKeyAndIdSelector(key: string, id: string): MemoizedSelector { + return keyAndIdSelector(key, id); } -export function keySelector(key: string): MemoizedSelector { - return createSelector(selectionStateSelector, (state: ObjectSelectionState) => { - if (hasValue(state)) { - return state[key]; +export function keyAndIdSelector(key: string, id: string): MemoizedSelector { + return createSelector(objectSelectionsStateSelector, (state: ObjectSelectionsState) => { + if (hasValue(state) && hasValue(state[key])) { + return state[key][id]; } else { return undefined; } diff --git a/src/app/shared/object-select/object-select/object-select.component.ts b/src/app/shared/object-select/object-select/object-select.component.ts index 58953d3abb..d99330a15b 100644 --- a/src/app/shared/object-select/object-select/object-select.component.ts +++ b/src/app/shared/object-select/object-select/object-select.component.ts @@ -11,6 +11,9 @@ import { ObjectSelectService } from '../object-select.service'; */ export abstract class ObjectSelectComponent implements OnInit, OnDestroy { + @Input() + key: string; + /** * The list of DSpaceObjects to display */ @@ -49,11 +52,11 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro } ngOnInit(): void { - this.selectedIds$ = this.objectSelectService.getAllSelected(); + this.selectedIds$ = this.objectSelectService.getAllSelected(this.key); } ngOnDestroy(): void { - this.objectSelectService.reset(); + this.objectSelectService.reset(this.key); } /** @@ -61,7 +64,7 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro * @param {string} id */ switch(id: string) { - this.objectSelectService.switch(id); + this.objectSelectService.switch(this.key, id); } /** @@ -70,7 +73,7 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro * @returns {Observable} */ getSelected(id: string): Observable { - return this.objectSelectService.getSelected(id); + return this.objectSelectService.getSelected(this.key, id); } /** @@ -82,7 +85,7 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro take(1) ).subscribe((ids: string[]) => { this.confirm.emit(ids); - this.objectSelectService.reset(); + this.objectSelectService.reset(this.key); }); } From 904ee2cab4d9a39672dadb4295d328a2f739e199 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 16 Nov 2018 14:21:48 +0100 Subject: [PATCH 42/78] 55693: Search box inside Map tab --- .../collection-item-mapper.component.html | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 2408f2540a..977c2c543d 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -5,16 +5,6 @@

{{'collection.item-mapper.description' | translate}}

-
-
- - -
-
- @@ -30,7 +20,17 @@ -
+
+
+ + +
+
+ +
Date: Fri, 16 Nov 2018 14:32:13 +0100 Subject: [PATCH 43/78] 55946: Search box inside Map tab on item-level --- .../item-collection-mapper.component.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 92cdc71a61..fab7a16d79 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -5,15 +5,6 @@

{{'item.edit.item-mapper.description' | translate}}

-
-
- - -
-
- @@ -29,7 +20,16 @@ -
+
+
+ + +
+
+ +
Date: Fri, 16 Nov 2018 15:38:49 +0100 Subject: [PATCH 44/78] 55946: Exclude already mapped collections from Map tab --- .../item-collection-mapper.component.ts | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 45755a52c0..3ab0d8b516 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -20,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { C } from '@angular/core/src/render3'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { isNotEmpty } from '../../../shared/empty.util'; @Component({ selector: 'ds-item-collection-mapper', @@ -82,9 +83,15 @@ export class ItemCollectionMapperComponent implements OnInit { map((itemRD: RemoteData) => itemRD.payload), switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); - this.mappingCollectionsRD$ = this.searchOptions$.pipe( - switchMap((searchOptions: PaginatedSearchOptions) => { - return this.searchService.search(Object.assign(searchOptions, { + + const itemCollectionsAndOptions$ = Observable.combineLatest( + this.itemCollectionsRD$, + this.searchOptions$ + ); + this.mappingCollectionsRD$ = itemCollectionsAndOptions$.pipe( + switchMap(([itemCollectionsRD, searchOptions]) => { + return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { + query: this.buildQuery(itemCollectionsRD.payload.page, searchOptions.query), dsoType: DSpaceObjectType.COLLECTION })); }), @@ -196,4 +203,31 @@ export class ItemCollectionMapperComponent implements OnInit { return this.router.url; } + /** + * Build a query to exclude collections from + * @param collections The collections their UUIDs + * @param query The query to add to it + */ + buildQuery(collections: Collection[], query: string): string { + let result = query; + for (const collection of collections) { + result = this.addExcludeCollection(collection.id, result); + } + return result; + } + + /** + * Add an exclusion of a collection to a query + * @param collectionId The collection's UUID + * @param query The query to add the exclusion to + */ + addExcludeCollection(collectionId: string, query: string): string { + const excludeQuery = `-search.resourceid:${collectionId}`; + if (isNotEmpty(query)) { + return `${query} AND ${excludeQuery}`; + } else { + return excludeQuery; + } + } + } From aa172b6c680f6104d32f2cc6c0605f54834806b5 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 23 Nov 2018 17:03:35 +0100 Subject: [PATCH 45/78] 55946: Changes to comply to the new response cache features --- .../collection-item-mapper.component.ts | 22 +++++++++--------- .../edit-item-page.component.ts | 5 ++-- .../item-collection-mapper.component.ts | 23 ++++++++----------- src/app/core/data/collection-data.service.ts | 2 +- ...ing-collections-reponse-parsing.service.ts | 2 +- .../item-select/item-select.component.ts | 7 +----- .../object-select/object-select.service.ts | 2 +- .../object-select/object-select.component.ts | 4 ++-- .../testing/object-select-service-stub.ts | 9 ++++---- 9 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 1bd39f4cc8..9490e22c6c 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -1,13 +1,14 @@ +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; + import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; -import { ActivatedRoute, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; -import { Observable } from 'rxjs/Observable'; import { Collection } from '../../core/shared/collection.model'; import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { flatMap, map, switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -15,11 +16,10 @@ import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ItemDataService } from '../../core/data/item-data.service'; -import { RestResponse } from '../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; import { CollectionDataService } from '../../core/data/collection-data.service'; -import { Item } from '../../core/shared/item.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { isNotEmpty } from '../../shared/empty.util'; +import { RestResponse } from '../../core/cache/response.models'; @Component({ selector: 'ds-collection-item-mapper', @@ -75,7 +75,7 @@ export class CollectionItemMapperComponent implements OnInit { } ngOnInit(): void { - this.collectionRD$ = this.route.data.map((data) => data.collection).pipe(getSucceededRemoteData()) as Observable>; + this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadItemLists(); } @@ -86,7 +86,7 @@ export class CollectionItemMapperComponent implements OnInit { * TODO: When the API support it, fetch items excluding the collection's scope (currently fetches all items) */ loadItemLists() { - const collectionAndOptions$ = Observable.combineLatest( + const collectionAndOptions$ = observableCombineLatest( this.collectionRD$, this.searchOptions$ ); @@ -120,7 +120,7 @@ export class CollectionItemMapperComponent implements OnInit { getSucceededRemoteData(), map((collectionRD: RemoteData) => collectionRD.payload.id), switchMap((collectionId: string) => - Observable.combineLatest(ids.map((id: string) => + observableCombineLatest(ids.map((id: string) => remove ? this.itemDataService.removeMappingFromCollection(id, collectionId) : this.itemDataService.mapToCollection(id, collectionId) )) ) @@ -132,7 +132,7 @@ export class CollectionItemMapperComponent implements OnInit { const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { - const successMessages = Observable.combineLatest( + const successMessages = observableCombineLatest( this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.head`), this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.content`, { amount: successful.length }) ); @@ -142,7 +142,7 @@ export class CollectionItemMapperComponent implements OnInit { }); } if (unsuccessful.length > 0) { - const unsuccessMessages = Observable.combineLatest( + const unsuccessMessages = observableCombineLatest( this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.head`), this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.content`, { amount: unsuccessful.length }) ); diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index 8bcf53f140..d276a15005 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -1,9 +1,10 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; +import { map } from 'rxjs/operators'; @Component({ selector: 'ds-edit-item-page', @@ -28,7 +29,7 @@ export class EditItemPageComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.map((data) => data.item); + this.itemRD$ = this.route.data.pipe(map((data) => data.item)); } } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 3ab0d8b516..633cb7037b 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -1,9 +1,8 @@ +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; + import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; -import { Observable } from 'rxjs/Observable'; import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Collection } from '../../../core/shared/collection.model'; @@ -13,14 +12,12 @@ import { ActivatedRoute, Router } from '@angular/router'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; import { map, switchMap } from 'rxjs/operators'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { RestResponse } from '../../../core/cache/response-cache.models'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { C } from '@angular/core/src/render3'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { isNotEmpty } from '../../../shared/empty.util'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-collection-mapper', @@ -68,7 +65,7 @@ export class ItemCollectionMapperComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadCollectionLists(); } @@ -84,7 +81,7 @@ export class ItemCollectionMapperComponent implements OnInit { switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); - const itemCollectionsAndOptions$ = Observable.combineLatest( + const itemCollectionsAndOptions$ = observableCombineLatest( this.itemCollectionsRD$, this.searchOptions$ ); @@ -104,7 +101,7 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {string[]} ids The list of collection UUID's to map the item to */ mapCollections(ids: string[]) { - const itemIdAndExcludingIds$ = Observable.combineLatest( + const itemIdAndExcludingIds$ = observableCombineLatest( this.itemRD$.pipe( getSucceededRemoteData(), map((rd: RemoteData) => rd.payload), @@ -119,7 +116,7 @@ export class ItemCollectionMapperComponent implements OnInit { // Map the item to the collections found in ids, excluding the collections the item is already mapped to const responses$ = itemIdAndExcludingIds$.pipe( - switchMap(([itemId, excludingIds]) => Observable.combineLatest(this.filterIds(ids, excludingIds).map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) + switchMap(([itemId, excludingIds]) => observableCombineLatest(this.filterIds(ids, excludingIds).map((id: string) => this.itemDataService.mapToCollection(itemId, id)))) ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); @@ -134,7 +131,7 @@ export class ItemCollectionMapperComponent implements OnInit { const responses$ = this.itemRD$.pipe( getSucceededRemoteData(), map((itemRD: RemoteData) => itemRD.payload.id), - switchMap((itemId: string) => Observable.combineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id)))) + switchMap((itemId: string) => observableCombineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id)))) ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); @@ -160,7 +157,7 @@ export class ItemCollectionMapperComponent implements OnInit { const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { - const successMessages = Observable.combineLatest( + const successMessages = observableCombineLatest( this.translateService.get(`${messagePrefix}.success.head`), this.translateService.get(`${messagePrefix}.success.content`, { amount: successful.length }) ); @@ -170,7 +167,7 @@ export class ItemCollectionMapperComponent implements OnInit { }); } if (unsuccessful.length > 0) { - const unsuccessMessages = Observable.combineLatest( + const unsuccessMessages = observableCombineLatest( this.translateService.get(`${messagePrefix}.error.head`), this.translateService.get(`${messagePrefix}.error.content`, { amount: unsuccessful.length }) ); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index d0479a9da6..59a6c8ca01 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -9,7 +9,7 @@ import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; import { distinctUntilChanged, map } from 'rxjs/operators'; diff --git a/src/app/core/data/mapping-collections-reponse-parsing.service.ts b/src/app/core/data/mapping-collections-reponse-parsing.service.ts index 0ae014301c..31200be3fb 100644 --- a/src/app/core/data/mapping-collections-reponse-parsing.service.ts +++ b/src/app/core/data/mapping-collections-reponse-parsing.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@angular/core'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { PaginatedList } from './paginated-list'; import { PageInfo } from '../shared/page-info.model'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; @Injectable() export class MappingCollectionsReponseParsingService implements ResponseParsingService { diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index d8d4eef34a..2cd5b502df 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -1,10 +1,5 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { take } from 'rxjs/operators'; -import { Observable } from 'rxjs/Observable'; +import { Component} from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; import { ObjectSelectService } from '../object-select.service'; import { ObjectSelectComponent } from '../object-select/object-select.component'; import { isNotEmpty } from '../../empty.util'; diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts index 2dbb69fe53..03ddc0078c 100644 --- a/src/app/shared/object-select/object-select.service.ts +++ b/src/app/shared/object-select/object-select.service.ts @@ -7,7 +7,7 @@ import { ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, ObjectSelectionSelectAction, ObjectSelectionSwitchAction } from './object-select.actions'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { hasValue } from '../empty.util'; import { map } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; diff --git a/src/app/shared/object-select/object-select/object-select.component.ts b/src/app/shared/object-select/object-select/object-select.component.ts index d99330a15b..d41cb771a6 100644 --- a/src/app/shared/object-select/object-select/object-select.component.ts +++ b/src/app/shared/object-select/object-select/object-select.component.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { take } from 'rxjs/operators'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; diff --git a/src/app/shared/testing/object-select-service-stub.ts b/src/app/shared/testing/object-select-service-stub.ts index f4bcccae77..7b3ee38752 100644 --- a/src/app/shared/testing/object-select-service-stub.ts +++ b/src/app/shared/testing/object-select-service-stub.ts @@ -1,4 +1,5 @@ -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; +import { of } from 'rxjs/internal/observable/of'; export class ObjectSelectServiceStub { @@ -12,14 +13,14 @@ export class ObjectSelectServiceStub { getSelected(id: string): Observable { if (this.ids.indexOf(id) > -1) { - return Observable.of(true); + return of(true); } else { - return Observable.of(false); + return of(false); } } getAllSelected(): Observable { - return Observable.of(this.ids); + return of(this.ids); } switch(id: string) { From e275fe590be583b5c038956f735eee793a83b43c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 23 Nov 2018 17:58:14 +0100 Subject: [PATCH 46/78] 55946: Remove href-to-uuid index cache on mapping --- .../collection-item-mapper.component.ts | 6 ++++- src/app/core/data/collection-data.service.ts | 17 ++++++++---- src/app/core/index/index.actions.ts | 27 ++++++++++++++++++- src/app/core/index/index.reducer.ts | 20 +++++++++++++- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 9490e22c6c..9199e010b8 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -8,7 +8,7 @@ import { Collection } from '../../core/shared/collection.model'; import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -152,6 +152,10 @@ export class CollectionItemMapperComponent implements OnInit { }); } }); + + this.collectionRD$.pipe(take(1)).subscribe((collectionRD: RemoteData) => { + this.collectionDataService.clearMappingItemsRequests(collectionRD.payload.id); + }); } /** diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 59a6c8ca01..6733681e0f 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -12,17 +12,17 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, take } from 'rxjs/operators'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { GetRequest } from './request.models'; -import { - configureRequest -} from '../shared/operators'; +import { configureRequest } from '../shared/operators'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from './parsing.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOResponseParsingService } from './dso-response-parsing.service'; +import { IndexName, IndexState } from '../index/index.reducer'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -34,7 +34,8 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected indexStore: Store ) { super(); } @@ -68,4 +69,10 @@ export class CollectionDataService extends ComColDataService { + this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); + }); + } + } diff --git a/src/app/core/index/index.actions.ts b/src/app/core/index/index.actions.ts index 014b6561a3..98d07d59d5 100644 --- a/src/app/core/index/index.actions.ts +++ b/src/app/core/index/index.actions.ts @@ -8,7 +8,8 @@ import { IndexName } from './index.reducer'; */ export const IndexActionTypes = { ADD: type('dspace/core/index/ADD'), - REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE') + REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE'), + REMOVE_BY_SUBSTRING: type('dspace/core/index/REMOVE_BY_SUBSTRING') }; /* tslint:disable:max-classes-per-file */ @@ -60,6 +61,30 @@ export class RemoveFromIndexByValueAction implements Action { this.payload = { name, value }; } +} + +/** + * An ngrx action to remove multiple values from the index by substring + */ +export class RemoveFromIndexBySubstringAction implements Action { + type = IndexActionTypes.REMOVE_BY_SUBSTRING; + payload: { + name: IndexName, + value: string + }; + + /** + * Create a new RemoveFromIndexByValueAction + * + * @param name + * the name of the index to remove from + * @param value + * the value to remove the UUID for + */ + constructor(name: IndexName, value: string) { + this.payload = { name, value }; + } + } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts index c179182509..dae7874794 100644 --- a/src/app/core/index/index.reducer.ts +++ b/src/app/core/index/index.reducer.ts @@ -2,7 +2,8 @@ import { IndexAction, IndexActionTypes, AddToIndexAction, - RemoveFromIndexByValueAction + RemoveFromIndexByValueAction, + RemoveFromIndexBySubstringAction } from './index.actions'; export enum IndexName { @@ -31,6 +32,10 @@ export function indexReducer(state = initialState, action: IndexAction): IndexSt return removeFromIndexByValue(state, action as RemoveFromIndexByValueAction) } + case IndexActionTypes.REMOVE_BY_SUBSTRING: { + return removeFromIndexBySubstring(state, action as RemoveFromIndexBySubstringAction) + } + default: { return state; } @@ -60,3 +65,16 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu [action.payload.name]: newSubState }); } + +function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState { + const subState = state[action.payload.name]; + const newSubState = Object.create(null); + for (const value in subState) { + if (value.indexOf(action.payload.value) < 0) { + newSubState[value] = subState[value]; + } + } + return Object.assign({}, state, { + [action.payload.name]: newSubState + }); +} From 4307cb0ff18e66f8936b52f8e4129dca86b55950 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 26 Nov 2018 13:18:17 +0100 Subject: [PATCH 47/78] 55946: Clear data/request attached to the hrefs deleted from index --- src/app/core/data/collection-data.service.ts | 9 ++-- src/app/core/data/request.actions.ts | 24 +++++++++- src/app/core/data/request.reducer.ts | 16 ++++++- src/app/core/data/request.service.ts | 49 ++++++++++++++++++-- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 6733681e0f..bc28ccc98c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { CoreState } from '../core.reducers'; +import { coreSelector, CoreState } from '../core.reducers'; import { Collection } from '../shared/collection.model'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; @@ -12,7 +12,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; -import { distinctUntilChanged, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { GetRequest } from './request.models'; import { configureRequest } from '../shared/operators'; @@ -23,6 +23,8 @@ import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { IndexName, IndexState } from '../index/index.reducer'; import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { pathSelector } from '../shared/selectors'; +import { RequestState } from './request.reducer'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -71,6 +73,7 @@ export class CollectionDataService extends ComColDataService { + this.requestService.removeByHrefSubstring(href); this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); }); } diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts index 28149c2ead..2b2de13504 100644 --- a/src/app/core/data/request.actions.ts +++ b/src/app/core/data/request.actions.ts @@ -10,7 +10,8 @@ export const RequestActionTypes = { CONFIGURE: type('dspace/core/data/request/CONFIGURE'), EXECUTE: type('dspace/core/data/request/EXECUTE'), COMPLETE: type('dspace/core/data/request/COMPLETE'), - RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS') + RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS'), + REMOVE: type('dspace/core/data/request/REMOVE') }; /* tslint:disable:max-classes-per-file */ @@ -82,6 +83,24 @@ export class ResetResponseTimestampsAction implements Action { } } +/** + * An ngrx action to remove a cached request + */ +export class RequestRemoveAction implements Action { + type = RequestActionTypes.REMOVE; + uuid: string; + + /** + * Create a new RequestRemoveAction + * + * @param uuid + * the request's uuid + */ + constructor(uuid: string) { + this.uuid = uuid + } +} + /* tslint:enable:max-classes-per-file */ /** @@ -91,4 +110,5 @@ export type RequestAction = RequestConfigureAction | RequestExecuteAction | RequestCompleteAction - | ResetResponseTimestampsAction; + | ResetResponseTimestampsAction + | RequestRemoveAction; diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index a680de2d6b..322ac46727 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -1,6 +1,6 @@ import { RequestActionTypes, RequestAction, RequestConfigureAction, - RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction + RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction, RequestRemoveAction } from './request.actions'; import { RestRequest } from './request.models'; import { RestResponse } from '../cache/response.models'; @@ -38,6 +38,10 @@ export function requestReducer(state = initialState, action: RequestAction): Req return resetResponseTimestamps(state, action as ResetResponseTimestampsAction); } + case RequestActionTypes.REMOVE: { + return removeRequest(state, action as RequestRemoveAction); + } + default: { return state; } @@ -95,3 +99,13 @@ function resetResponseTimestamps(state: RequestState, action: ResetResponseTimes }); return newState; } + +function removeRequest(state: RequestState, action: RequestRemoveAction): RequestState { + const newState = Object.create(null); + for (const value in state) { + if (value !== action.uuid) { + newState[value] = state[value]; + } + } + return newState; +} diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 285ed06545..a447df3051 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -15,16 +15,16 @@ import { import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { hasNoValue, hasValue, isNotUndefined } from '../../shared/empty.util'; +import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; +import { hasNoValue, hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { coreSelector, CoreState } from '../core.reducers'; -import { IndexName } from '../index/index.reducer'; +import { IndexName, IndexState } from '../index/index.reducer'; import { pathSelector } from '../shared/selectors'; import { UUIDService } from '../shared/uuid.service'; -import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; +import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; import { GetRequest, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; @@ -54,6 +54,25 @@ export class RequestService { return pathSelector(coreSelector, 'index', IndexName.UUID_MAPPING, uuid); } + private uuidsFromHrefSubstringSelector(selector: MemoizedSelector, name: string, href: string): MemoizedSelector { + return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href)); + } + + private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] { + let result = []; + if (isNotEmpty(state)) { + const subState = state[name]; + if (isNotEmpty(subState)) { + for (const value in subState) { + if (value.indexOf(href) > -1) { + result = [...result, subState[value]]; + } + } + } + } + return result; + } + generateRequestId(): string { return `client/${this.uuidService.generate()}`; } @@ -119,6 +138,28 @@ export class RequestService { } } + removeByHref(href: string) { + this.store.pipe( + select(this.uuidFromHrefSelector(href)) + ).subscribe((uuid: string) => { + this.removeByUuid(uuid); + }); + } + + removeByHrefSubstring(href: string) { + this.store.pipe( + select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)) + ).subscribe((uuids: string[]) => { + for (const uuid of uuids) { + this.removeByUuid(uuid); + } + }); + } + + removeByUuid(uuid: string) { + this.store.dispatch(new RequestRemoveAction(uuid)); + } + /** * Check if a request is in the cache or if it's still pending * @param {GetRequest} request The request to check From 55e37a63bc91c95daeaa03959be99301df7d6b3c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 27 Nov 2018 14:20:24 +0100 Subject: [PATCH 48/78] 55946: discovery requests reset and reload and list updates --- .../collection-item-mapper.component.ts | 40 +++++++++++++------ .../search-service/search.service.ts | 11 ++++- .../builders/remote-data-build.service.ts | 6 +-- src/app/core/data/collection-data.service.ts | 16 ++++---- src/app/core/data/item-data.service.ts | 7 ++-- src/app/core/data/request.service.ts | 18 ++++----- .../object-collection.component.ts | 1 - 7 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 9199e010b8..8058398884 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -8,7 +8,7 @@ import { Collection } from '../../core/shared/collection.model'; import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -20,6 +20,10 @@ import { TranslateService } from '@ngx-translate/core'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { isNotEmpty } from '../../shared/empty.util'; import { RestResponse } from '../../core/cache/response.models'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { Actions, ofType } from '@ngrx/effects'; +import { IndexActionTypes } from '../../core/index/index.actions'; +import { RequestActionTypes } from '../../core/data/request.actions'; @Component({ selector: 'ds-collection-item-mapper', @@ -64,6 +68,8 @@ export class CollectionItemMapperComponent implements OnInit { */ defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); + shouldUpdate$: BehaviorSubject; + constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, @@ -86,25 +92,31 @@ export class CollectionItemMapperComponent implements OnInit { * TODO: When the API support it, fetch items excluding the collection's scope (currently fetches all items) */ loadItemLists() { + this.shouldUpdate$ = new BehaviorSubject(true); const collectionAndOptions$ = observableCombineLatest( this.collectionRD$, - this.searchOptions$ + this.searchOptions$, + this.shouldUpdate$ ); this.collectionItemsRD$ = collectionAndOptions$.pipe( - switchMap(([collectionRD, options]) => { - return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { - sort: this.defaultSortOptions - })) + switchMap(([collectionRD, options, shouldUpdate]) => { + if (shouldUpdate) { + return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, { + sort: this.defaultSortOptions + })) + } }) ); this.mappingItemsRD$ = collectionAndOptions$.pipe( - switchMap(([collectionRD, options]) => { - return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { - query: this.buildQuery(collectionRD.payload.id, options.query), - scope: undefined, - dsoType: DSpaceObjectType.ITEM, - sort: this.defaultSortOptions - })); + switchMap(([collectionRD, options, shouldUpdate]) => { + if (shouldUpdate) { + return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { + query: this.buildQuery(collectionRD.payload.id, options.query), + scope: undefined, + dsoType: DSpaceObjectType.ITEM, + sort: this.defaultSortOptions + })); + } }), toDSpaceObjectListRD() ); @@ -151,10 +163,12 @@ export class CollectionItemMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } + this.shouldUpdate$.next(true); }); this.collectionRD$.pipe(take(1)).subscribe((collectionRD: RemoteData) => { this.collectionDataService.clearMappingItemsRequests(collectionRD.payload.id); + this.searchService.clearDiscoveryRequests(); }); } diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 275b0b3340..049e4b1542 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -7,7 +7,7 @@ import { Router, UrlSegmentGroup } from '@angular/router'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { FacetConfigSuccessResponse, @@ -47,6 +47,9 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { ResourceType } from '../../core/shared/resource-type'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { Store } from '@ngrx/store'; +import { IndexName, IndexState } from '../../core/index/index.reducer'; +import { RemoveFromIndexBySubstringAction } from '../../core/index/index.actions'; /** * Service that performs all general actions that have to do with the search page @@ -319,6 +322,12 @@ export class SearchService implements OnDestroy { return '/' + g.toString(); } + clearDiscoveryRequests() { + this.halService.getEndpoint(this.searchLinkPath).pipe(take(1)).subscribe((href: string) => { + this.requestService.removeByHrefSubstring(href); + }); + } + /** * Unsubscribe from the subscription */ diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 52ec4382ae..c22b63f618 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -6,7 +6,7 @@ import { } from 'rxjs'; import { Injectable } from '@angular/core'; import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; @@ -86,8 +86,8 @@ export class RemoteDataBuildService { toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { return observableCombineLatest(requestEntry$, payload$).pipe( map(([reqEntry, payload]) => { - const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; - const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; + const requestPending = hasValue(reqEntry) && hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; + const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; let isSuccessful: boolean; let error: RemoteDataError; if (hasValue(reqEntry) && hasValue(reqEntry.response)) { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index bc28ccc98c..e9ae5d38a5 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; +import { Action, Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { coreSelector, CoreState } from '../core.reducers'; +import { CoreState } from '../core.reducers'; import { Collection } from '../shared/collection.model'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; @@ -12,7 +12,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; -import { distinctUntilChanged, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; import { GetRequest } from './request.models'; import { configureRequest } from '../shared/operators'; @@ -22,9 +22,8 @@ import { ResponseParsingService } from './parsing.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { IndexName, IndexState } from '../index/index.reducer'; -import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { pathSelector } from '../shared/selectors'; -import { RequestState } from './request.reducer'; +import { IndexActionTypes, RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { Actions, ofType } from '@ngrx/effects'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -50,6 +49,8 @@ export class CollectionDataService extends ComColDataService>> { + const requestUuid = this.requestService.generateRequestId(); + const href$ = this.getMappingItemsEndpoint(collectionId).pipe( isNotEmptyOperator(), distinctUntilChanged(), @@ -58,7 +59,7 @@ export class CollectionDataService extends ComColDataService { - const request = new GetRequest(this.requestService.generateRequestId(), endpoint); + const request = new GetRequest(requestUuid, endpoint); return Object.assign(request, { getResponseParser(): GenericConstructor { return DSOResponseParsingService; @@ -74,7 +75,6 @@ export class CollectionDataService extends ComColDataService { this.requestService.removeByHrefSubstring(href); - this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); }); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 15dc01d03a..c1a46cde9a 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,5 +1,5 @@ -import { distinctUntilChanged, map, filter, switchMap } from 'rxjs/operators'; +import { distinctUntilChanged, map, filter, switchMap, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -21,6 +21,7 @@ import { configureRequest, filterSuccessfulResponses, getResponseFromEntry } fro import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; import { Collection } from '../shared/collection.model'; +import { RequestEntry } from './request.reducer'; @Injectable() export class ItemDataService extends DataService { @@ -66,7 +67,7 @@ export class ItemDataService extends DataService { distinctUntilChanged(), map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), - switchMap((request: RestRequest) => this.requestService.getByHref(request.href)), + switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)), getResponseFromEntry() ); } @@ -77,7 +78,7 @@ export class ItemDataService extends DataService { distinctUntilChanged(), map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), - switchMap((request: RestRequest) => this.requestService.getByHref(request.href)), + switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)), getResponseFromEntry() ); } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index a447df3051..9e8f28e1b9 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -31,7 +31,7 @@ import { RequestEntry } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; import { getResponseFromEntry } from '../shared/operators'; -import { AddToIndexAction } from '../index/index.actions'; +import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions'; @Injectable() export class RequestService { @@ -39,7 +39,8 @@ export class RequestService { constructor(private objectCache: ObjectCacheService, private uuidService: UUIDService, - private store: Store) { + private store: Store, + private indexStore: Store) { } private entryFromUUIDSelector(uuid: string): MemoizedSelector { @@ -138,22 +139,17 @@ export class RequestService { } } - removeByHref(href: string) { - this.store.pipe( - select(this.uuidFromHrefSelector(href)) - ).subscribe((uuid: string) => { - this.removeByUuid(uuid); - }); - } - removeByHrefSubstring(href: string) { this.store.pipe( - select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)) + select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)), + take(1) ).subscribe((uuids: string[]) => { for (const uuid of uuids) { this.removeByUuid(uuid); } }); + this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0); + this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); } removeByUuid(uuid: string) { diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index 4e46aeaeab..0018c55c7f 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -77,7 +77,6 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { this.currentMode = params.view; } }); - console.log(this.objects); } /** From 2053078a16171ff46f210140ec3932a4ee746747 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 27 Nov 2018 14:54:53 +0100 Subject: [PATCH 49/78] 55946: Dynamic reloading of item mapper on item-level --- .../collection-item-mapper.component.ts | 20 ++++++++++++ .../item-collection-mapper.component.ts | 32 +++++++++++++++++-- .../search-service/search.service.ts | 3 ++ src/app/core/data/collection-data.service.ts | 8 ++--- src/app/core/data/item-data.service.ts | 8 ++++- src/app/core/data/request.service.ts | 9 ++++++ 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 8058398884..4d30a761e9 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -68,6 +68,10 @@ export class CollectionItemMapperComponent implements OnInit { */ defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC); + /** + * Firing this observable (shouldUpdate$.next(true)) forces the two lists to reload themselves + * Usually fired after the lists their cache is cleared (to force a new request to the REST API) + */ shouldUpdate$: BehaviorSubject; constructor(private route: ActivatedRoute, @@ -138,6 +142,16 @@ export class CollectionItemMapperComponent implements OnInit { ) ); + this.showNotifications(responses$, remove); + this.clearRequestCache(); + } + + /** + * Display notifications + * @param {Observable} responses$ The responses after adding/removing a mapping + * @param {boolean} remove Whether or not the goal was to remove mappings + */ + private showNotifications(responses$: Observable, remove?: boolean) { const messageInsertion = remove ? 'unmap' : 'map'; responses$.subscribe((responses: RestResponse[]) => { @@ -163,9 +177,15 @@ export class CollectionItemMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } + // Force an update on all lists this.shouldUpdate$.next(true); }); + } + /** + * Clear all previous requests from cache in preparation of refreshing all lists + */ + private clearRequestCache() { this.collectionRD$.pipe(take(1)).subscribe((collectionRD: RemoteData) => { this.collectionDataService.clearMappingItemsRequests(collectionRD.payload.id); this.searchService.clearDiscoveryRequests(); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 633cb7037b..0b68864bce 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -11,13 +11,14 @@ import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shar import { ActivatedRoute, Router } from '@angular/router'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { isNotEmpty } from '../../../shared/empty.util'; import { RestResponse } from '../../../core/cache/response.models'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; @Component({ selector: 'ds-item-collection-mapper', @@ -55,6 +56,12 @@ export class ItemCollectionMapperComponent implements OnInit { */ mappingCollectionsRD$: Observable>>; + /** + * Firing this observable (shouldUpdate$.next(true)) forces the two lists to reload themselves + * Usually fired after the lists their cache is cleared (to force a new request to the REST API) + */ + shouldUpdate$: BehaviorSubject; + constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, @@ -76,8 +83,13 @@ export class ItemCollectionMapperComponent implements OnInit { * TODO: When the API support it, fetch collections excluding the item's scope (currently fetches all collections) */ loadCollectionLists() { - this.itemCollectionsRD$ = this.itemRD$.pipe( - map((itemRD: RemoteData) => itemRD.payload), + this.shouldUpdate$ = new BehaviorSubject(true); + this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$, this.shouldUpdate$).pipe( + map(([itemRD, shouldUpdate]) => { + if (shouldUpdate) { + return itemRD.payload + } + }), switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); @@ -120,6 +132,7 @@ export class ItemCollectionMapperComponent implements OnInit { ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); + this.clearRequestCache(); } /** @@ -135,6 +148,7 @@ export class ItemCollectionMapperComponent implements OnInit { ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); + this.clearRequestCache(); } /** @@ -176,6 +190,18 @@ export class ItemCollectionMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } + // Force an update on all lists + this.shouldUpdate$.next(true); + }); + } + + /** + * Clear all previous requests from cache in preparation of refreshing all lists + */ + private clearRequestCache() { + this.itemRD$.pipe(take(1)).subscribe((itemRD: RemoteData) => { + this.itemDataService.clearMappedCollectionsRequests(itemRD.payload.id); + this.searchService.clearDiscoveryRequests(); }); } diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 049e4b1542..194adfa22f 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -322,6 +322,9 @@ export class SearchService implements OnDestroy { return '/' + g.toString(); } + /** + * Clear all request cache related to discovery objects + */ clearDiscoveryRequests() { this.halService.getEndpoint(this.searchLinkPath).pipe(take(1)).subscribe((href: string) => { this.requestService.removeByHrefSubstring(href); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index e9ae5d38a5..3882f8e37c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Action, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -21,9 +21,6 @@ import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from './parsing.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { IndexName, IndexState } from '../index/index.reducer'; -import { IndexActionTypes, RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { Actions, ofType } from '@ngrx/effects'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -35,8 +32,7 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService, - protected indexStore: Store + protected objectCache: ObjectCacheService ) { super(); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c1a46cde9a..df36bad0bf 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,5 +1,5 @@ -import { distinctUntilChanged, map, filter, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, filter, switchMap, tap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -102,4 +102,10 @@ export class ItemDataService extends DataService { return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); } + public clearMappedCollectionsRequests(itemId: string) { + this.getMappingCollectionsEndpoint(itemId).pipe(take(1)).subscribe((href: string) => { + this.requestService.removeByHrefSubstring(href); + }); + } + } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 9e8f28e1b9..922f035139 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -139,6 +139,11 @@ export class RequestService { } } + /** + * Remove all request cache providing (part of) the href + * This also includes href-to-uuid index cache + * @param href A substring of the request(s) href + */ removeByHrefSubstring(href: string) { this.store.pipe( select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)), @@ -152,6 +157,10 @@ export class RequestService { this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); } + /** + * Remove request cache using the request's UUID + * @param uuid + */ removeByUuid(uuid: string) { this.store.dispatch(new RequestRemoveAction(uuid)); } From 930af49030878ae19ec4bcce98f593abf12daa78 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 27 Nov 2018 16:45:53 +0100 Subject: [PATCH 50/78] 55946: Fixed tests and added JSDocs --- .../collection-item-mapper.component.spec.ts | 23 ++++++++++----- .../item-collection-mapper.component.spec.ts | 29 +++++++++++-------- src/app/core/data/collection-data.service.ts | 13 +++++++++ src/app/core/data/item-data.service.ts | 24 +++++++++++++++ .../collection-select.component.html | 2 +- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 0007a96195..9d4f6e8f7b 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -20,7 +20,6 @@ import { FormsModule } from '@angular/forms'; import { SharedModule } from '../../shared/shared.module'; import { Collection } from '../../core/shared/collection.model'; import { RemoteData } from '../../core/data/remote-data'; -import { Observable } from 'rxjs/Observable'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; @@ -28,7 +27,6 @@ import { EventEmitter, NgModule } from '@angular/core'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; import { By } from '@angular/platform-browser'; -import { RestResponse } from '../../core/cache/response-cache.models'; import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; @@ -38,6 +36,9 @@ 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 { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { RestResponse } from '../../core/cache/response.models'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -55,7 +56,7 @@ describe('CollectionItemMapperComponent', () => { name: 'test-collection' }); const mockCollectionRD: RemoteData = new RemoteData(false, false, true, null, mockCollection); - const mockSearchOptions = Observable.of(new PaginatedSearchOptions({ + const mockSearchOptions = of(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, @@ -71,21 +72,27 @@ describe('CollectionItemMapperComponent', () => { paginatedSearchOptions: mockSearchOptions }; const itemDataServiceStub = { - mapToCollection: () => Observable.of(new RestResponse(true, '200')) + mapToCollection: () => of(new RestResponse(true, '200')) }; const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD }); const translateServiceStub = { - get: () => Observable.of('test-message of collection ' + mockCollection.name), + get: () => of('test-message of collection ' + mockCollection.name), onLangChange: new EventEmitter(), onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() }; const emptyList = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); const searchServiceStub = Object.assign(new SearchServiceStub(), { - search: () => Observable.of(emptyList) + search: () => of(emptyList), + /* tslint:disable:no-empty */ + clearDiscoveryRequests: () => {} + /* tslint:enable:no-empty */ }); const collectionDataServiceStub = { - getMappedItems: () => Observable.of(emptyList) + getMappedItems: () => of(emptyList), + /* tslint:disable:no-empty */ + clearMappingItemsRequests: () => {} + /* tslint:enable:no-empty */ }; beforeEach(async(() => { @@ -139,7 +146,7 @@ describe('CollectionItemMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, '404'))); comp.mapItems(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index fcd9a18d49..79bcffe166 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -8,14 +8,11 @@ import { SearchConfigurationService } from '../../../+search-page/search-service import { SearchService } from '../../../+search-page/search-service/search.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { Collection } from '../../../core/shared/collection.model'; import { RemoteData } from '../../../core/data/remote-data'; -import { Observable } from 'rxjs/Observable'; import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { RouterStub } from '../../../shared/testing/router-stub'; -import { RestResponse } from '../../../core/cache/response-cache.models'; import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; import { EventEmitter } from '@angular/core'; import { SearchServiceStub } from '../../../shared/testing/search-service-stub'; @@ -29,9 +26,11 @@ import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub'; import { By } from '@angular/platform-browser'; import { Item } from '../../../core/shared/item.model'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; import { ObjectSelectService } from '../../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub'; +import { Observable } from 'rxjs/internal/Observable'; +import { of } from 'rxjs/internal/observable/of'; +import { RestResponse } from '../../../core/cache/response.models'; describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; @@ -49,7 +48,7 @@ describe('ItemCollectionMapperComponent', () => { name: 'test-item' }); const mockItemRD: RemoteData = new RemoteData(false, false, true, null, mockItem); - const mockSearchOptions = Observable.of(new PaginatedSearchOptions({ + const mockSearchOptions = of(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, @@ -65,16 +64,22 @@ describe('ItemCollectionMapperComponent', () => { }; const mockCollectionsRD = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); const itemDataServiceStub = { - mapToCollection: () => Observable.of(new RestResponse(true, '200')), - removeMappingFromCollection: () => Observable.of(new RestResponse(true, '200')), - getMappedCollections: () => Observable.of(mockCollectionsRD) + mapToCollection: () => of(new RestResponse(true, '200')), + removeMappingFromCollection: () => of(new RestResponse(true, '200')), + getMappedCollections: () => of(mockCollectionsRD), + /* tslint:disable:no-empty */ + clearMappedCollectionsRequests: () => {} + /* tslint:enable:no-empty */ }; const searchServiceStub = Object.assign(new SearchServiceStub(), { - search: () => Observable.of(mockCollectionsRD) + search: () => of(mockCollectionsRD), + /* tslint:disable:no-empty */ + clearDiscoveryRequests: () => {} + /* tslint:enable:no-empty */ }); const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD }); const translateServiceStub = { - get: () => Observable.of('test-message of item ' + mockItem.name), + get: () => of('test-message of item ' + mockItem.name), onLangChange: new EventEmitter(), onTranslationChange: new EventEmitter(), onDefaultLangChange: new EventEmitter() @@ -130,7 +135,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, '404'))); comp.mapCollections(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); @@ -152,7 +157,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if the removal of at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(Observable.of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(of(new RestResponse(false, '404'))); comp.removeMappings(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 3882f8e37c..43a66b41bd 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -37,6 +37,10 @@ export class CollectionDataService extends ComColDataService { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getFindByIDHref(endpoint, collectionId)), @@ -44,6 +48,11 @@ export class CollectionDataService extends ComColDataService>> { const requestUuid = this.requestService.generateRequestId(); @@ -68,6 +77,10 @@ export class CollectionDataService extends ComColDataService { this.requestService.removeByHrefSubstring(href); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index df36bad0bf..50f2ebe45e 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -54,6 +54,12 @@ export class ItemDataService extends DataService { distinctUntilChanged(),); } + /** + * Fetches the endpoint used for mapping an item to a collection, + * or for fetching all collections the item is mapped to if no collection is provided + * @param itemId The item's id + * @param collectionId The collection's id (optional) + */ public getMappingCollectionsEndpoint(itemId: string, collectionId?: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)), @@ -61,6 +67,11 @@ export class ItemDataService extends DataService { ); } + /** + * Removes the mapping of an item from a collection + * @param itemId The item's id + * @param collectionId The collection's id + */ public removeMappingFromCollection(itemId: string, collectionId: string): Observable { return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), @@ -72,6 +83,11 @@ export class ItemDataService extends DataService { ); } + /** + * Maps an item to a collection + * @param itemId The item's id + * @param collectionId The collection's id + */ public mapToCollection(itemId: string, collectionId: string): Observable { return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), @@ -83,6 +99,10 @@ export class ItemDataService extends DataService { ); } + /** + * Fetches all collections the item is mapped to + * @param itemId The item's id + */ public getMappedCollections(itemId: string): Observable>> { const request$ = this.getMappingCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), @@ -102,6 +122,10 @@ export class ItemDataService extends DataService { return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); } + /** + * Clears all requests (from cache) connected to the mappingCollections endpoint + * @param itemId + */ public clearMappedCollectionsRequests(itemId: string) { this.getMappingCollectionsEndpoint(itemId).pipe(take(1)).subscribe((href: string) => { this.requestService.removeByHrefSubstring(href); diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index 551d33ba3b..d53a030baf 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -17,7 +17,7 @@ - {{collection.name}} + {{collection.name}} From 32db97e67d61ecb031f75e84aa65a5a6cb9b3479 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 30 Nov 2018 17:31:09 +0100 Subject: [PATCH 51/78] 55946: Spec file fixes --- src/app/core/data/request.service.spec.ts | 8 ++- .../core/metadata/metadata.service.spec.ts | 5 +- .../collection-select.component.spec.ts | 4 +- .../item-select/item-select.component.spec.ts | 8 +-- .../object-select.reducer.spec.ts | 53 +++++++++++-------- .../object-select.service.spec.ts | 42 +++++++++------ 6 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 90d2edfc84..e150d3c458 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -21,6 +21,7 @@ import { RequestService } from './request.service'; import { ActionsSubject, Store } from '@ngrx/store'; import { TestScheduler } from 'rxjs/testing'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { IndexState } from '../index/index.reducer'; describe('RequestService', () => { let scheduler: TestScheduler; @@ -29,6 +30,7 @@ describe('RequestService', () => { let objectCache: ObjectCacheService; let uuidService: UUIDService; let store: Store; + let indexStore: Store; const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'; const testHref = 'https://rest.api/endpoint/selfLink'; @@ -48,7 +50,8 @@ describe('RequestService', () => { uuidService = getMockUUIDService(); - store = new Store(new BehaviorSubject({}), new ActionsSubject(), null); + store = new Store(undefined, new ActionsSubject(), null); + indexStore = new Store(undefined, new ActionsSubject(), null); selectSpy = spyOnProperty(ngrx, 'select'); selectSpy.and.callFake(() => { return () => { @@ -59,7 +62,8 @@ describe('RequestService', () => { service = new RequestService( objectCache, uuidService, - store + store, + indexStore ); serviceAsAny = service as any; }); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 3be50a7450..78b204249b 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -32,6 +32,7 @@ import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { EmptyError } from 'rxjs/internal-compatibility'; +import { IndexState } from '../index/index.reducer'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -60,6 +61,7 @@ describe('MetadataService', () => { let title: Title; let store: Store; + let indexStore: Store; let objectCacheService: ObjectCacheService; let requestService: RequestService; @@ -78,11 +80,12 @@ describe('MetadataService', () => { beforeEach(() => { store = new Store(undefined, undefined, undefined); + indexStore = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); objectCacheService = new ObjectCacheService(store); uuidService = new UUIDService(); - requestService = new RequestService(objectCacheService, uuidService, store); + requestService = new RequestService(objectCacheService, uuidService, store, indexStore); remoteDataBuildService = new RemoteDataBuildService(objectCacheService, requestService); TestBed.configureTestingModule({ diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index 477f823928..bc83c3d52a 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -1,6 +1,5 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; @@ -15,6 +14,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { CollectionSelectComponent } from './collection-select.component'; import { Collection } from '../../../core/shared/collection.model'; +import { of } from 'rxjs/internal/observable/of'; describe('ItemSelectComponent', () => { let comp: CollectionSelectComponent; @@ -31,7 +31,7 @@ describe('ItemSelectComponent', () => { name: 'name2' }) ]; - const mockCollections = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockCollectionList))); + const mockCollections = of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockCollectionList))); const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index e07858360e..be7c315c45 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -2,7 +2,6 @@ import { ItemSelectComponent } from './item-select.component'; import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../../core/shared/item.model'; -import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; @@ -15,6 +14,7 @@ import { HostWindowService } from '../../host-window.service'; import { HostWindowServiceStub } from '../../testing/host-window-service-stub'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { of } from 'rxjs/internal/observable/of'; describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; @@ -24,7 +24,7 @@ describe('ItemSelectComponent', () => { const mockItemList = [ Object.assign(new Item(), { id: 'id1', - bitstreams: Observable.of({}), + bitstreams: of({}), metadata: [ { key: 'dc.title', @@ -39,7 +39,7 @@ describe('ItemSelectComponent', () => { }), Object.assign(new Item(), { id: 'id2', - bitstreams: Observable.of({}), + bitstreams: of({}), metadata: [ { key: 'dc.title', @@ -53,7 +53,7 @@ describe('ItemSelectComponent', () => { }] }) ]; - const mockItems = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); + const mockItems = of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList))); const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), { id: 'search-page-configuration', pageSize: 10, diff --git a/src/app/shared/object-select/object-select.reducer.spec.ts b/src/app/shared/object-select/object-select.reducer.spec.ts index 696df97d39..197cbed510 100644 --- a/src/app/shared/object-select/object-select.reducer.spec.ts +++ b/src/app/shared/object-select/object-select.reducer.spec.ts @@ -5,6 +5,7 @@ import { } from './object-select.actions'; import { objectSelectionReducer } from './object-select.reducer'; +const key = 'key'; const objectId1 = 'id1'; const objectId2 = 'id2'; @@ -12,7 +13,7 @@ class NullAction extends ObjectSelectionSelectAction { type = null; constructor() { - super(undefined); + super(undefined, undefined); } } @@ -20,7 +21,8 @@ describe('objectSelectionReducer', () => { it('should return the current state when no valid actions have been made', () => { const state = {}; - state[objectId1] = { checked: true }; + state[key] = {}; + state[key][objectId1] = { checked: true }; const action = new NullAction(); const newState = objectSelectionReducer(state, action); @@ -36,63 +38,68 @@ describe('objectSelectionReducer', () => { }); it('should set checked to true in response to the INITIAL_SELECT action', () => { - const action = new ObjectSelectionInitialSelectAction(objectId1); + const action = new ObjectSelectionInitialSelectAction(key, objectId1); const newState = objectSelectionReducer(undefined, action); - expect(newState[objectId1].checked).toBeTruthy(); + expect(newState[key][objectId1].checked).toBeTruthy(); }); it('should set checked to true in response to the INITIAL_DESELECT action', () => { - const action = new ObjectSelectionInitialDeselectAction(objectId1); + const action = new ObjectSelectionInitialDeselectAction(key, objectId1); const newState = objectSelectionReducer(undefined, action); - expect(newState[objectId1].checked).toBeFalsy(); + expect(newState[key][objectId1].checked).toBeFalsy(); }); it('should set checked to true in response to the SELECT action', () => { const state = {}; - state[objectId1] = { checked: false }; - const action = new ObjectSelectionSelectAction(objectId1); + state[key] = {}; + state[key][objectId1] = { checked: false }; + const action = new ObjectSelectionSelectAction(key, objectId1); const newState = objectSelectionReducer(state, action); - expect(newState[objectId1].checked).toBeTruthy(); + expect(newState[key][objectId1].checked).toBeTruthy(); }); it('should set checked to false in response to the DESELECT action', () => { const state = {}; - state[objectId1] = { checked: true }; - const action = new ObjectSelectionDeselectAction(objectId1); + state[key] = {}; + state[key][objectId1] = { checked: true }; + const action = new ObjectSelectionDeselectAction(key, objectId1); const newState = objectSelectionReducer(state, action); - expect(newState[objectId1].checked).toBeFalsy(); + expect(newState[key][objectId1].checked).toBeFalsy(); }); it('should set checked from false to true in response to the SWITCH action', () => { const state = {}; - state[objectId1] = { checked: false }; - const action = new ObjectSelectionSwitchAction(objectId1); + state[key] = {}; + state[key][objectId1] = { checked: false }; + const action = new ObjectSelectionSwitchAction(key, objectId1); const newState = objectSelectionReducer(state, action); - expect(newState[objectId1].checked).toBeTruthy(); + expect(newState[key][objectId1].checked).toBeTruthy(); }); it('should set checked from true to false in response to the SWITCH action', () => { const state = {}; - state[objectId1] = { checked: true }; - const action = new ObjectSelectionSwitchAction(objectId1); + state[key] = {}; + state[key][objectId1] = { checked: true }; + const action = new ObjectSelectionSwitchAction(key, objectId1); const newState = objectSelectionReducer(state, action); - expect(newState[objectId1].checked).toBeFalsy(); + expect(newState[key][objectId1].checked).toBeFalsy(); }); - it('should set reset the state in response to the RESET action', () => { + it('should reset the state in response to the RESET action', () => { const state = {}; - state[objectId1] = { checked: true }; - state[objectId2] = { checked: false }; - const action = new ObjectSelectionResetAction(undefined); + state[key] = {}; + state[key][objectId1] = { checked: true }; + state[key][objectId2] = { checked: false }; + const action = new ObjectSelectionResetAction(key, undefined); const newState = objectSelectionReducer(state, action); - expect(newState).toEqual({}); + expect(newState[key]).toEqual({}); }); }); diff --git a/src/app/shared/object-select/object-select.service.spec.ts b/src/app/shared/object-select/object-select.service.spec.ts index 3b5bcec06f..ea4b99c419 100644 --- a/src/app/shared/object-select/object-select.service.spec.ts +++ b/src/app/shared/object-select/object-select.service.spec.ts @@ -1,7 +1,6 @@ import { ObjectSelectService } from './object-select.service'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; -import { ObjectSelectionsState } from './object-select.reducer'; +import { ObjectSelectionListState, ObjectSelectionsState } from './object-select.reducer'; import { AppState } from '../../app.reducer'; import { ObjectSelectionDeselectAction, @@ -9,87 +8,96 @@ import { ObjectSelectionInitialSelectAction, ObjectSelectionResetAction, ObjectSelectionSelectAction, ObjectSelectionSwitchAction } from './object-select.actions'; +import { of } from 'rxjs/internal/observable/of'; describe('ObjectSelectService', () => { let service: ObjectSelectService; + const mockKey = 'key'; const mockObjectId = 'id1'; + const selectionStore: Store = jasmine.createSpyObj('selectionStore', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: of(true) + }); + const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, /* tslint:enable:no-empty */ - select: Observable.of(true) + select: of(true) }); const appStore: Store = jasmine.createSpyObj('appStore', { /* tslint:disable:no-empty */ dispatch: {}, /* tslint:enable:no-empty */ - select: Observable.of(true) + select: of(true) }); beforeEach(() => { - service = new ObjectSelectService(store, appStore); + service = new ObjectSelectService(selectionStore, appStore); }); describe('when the initialSelect method is triggered', () => { beforeEach(() => { - service.initialSelect(mockObjectId); + service.initialSelect(mockKey, mockObjectId); }); it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialSelectAction(mockObjectId)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialSelectAction(mockKey, mockObjectId)); }); }); describe('when the initialDeselect method is triggered', () => { beforeEach(() => { - service.initialDeselect(mockObjectId); + service.initialDeselect(mockKey, mockObjectId); }); it('ObjectSelectionInitialDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialDeselectAction(mockObjectId)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialDeselectAction(mockKey, mockObjectId)); }); }); describe('when the select method is triggered', () => { beforeEach(() => { - service.select(mockObjectId); + service.select(mockKey, mockObjectId); }); it('ObjectSelectionSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSelectAction(mockObjectId)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionSelectAction(mockKey, mockObjectId)); }); }); describe('when the deselect method is triggered', () => { beforeEach(() => { - service.deselect(mockObjectId); + service.deselect(mockKey, mockObjectId); }); it('ObjectSelectionDeselectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionDeselectAction(mockObjectId)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionDeselectAction(mockKey, mockObjectId)); }); }); describe('when the switch method is triggered', () => { beforeEach(() => { - service.switch(mockObjectId); + service.switch(mockKey, mockObjectId); }); it('ObjectSelectionSwitchAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionSwitchAction(mockObjectId)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionSwitchAction(mockKey, mockObjectId)); }); }); describe('when the reset method is triggered', () => { beforeEach(() => { - service.reset(); + service.reset(mockKey); }); it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new ObjectSelectionResetAction(null)); + expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionResetAction(mockKey, null)); }); }); From 791325f584d90802e76acbd64c08d65e5f558a81 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Dec 2018 17:17:54 +0100 Subject: [PATCH 52/78] 55946: TSLint fixes --- src/app/+community-page/community-page.component.ts | 5 ----- src/app/core/config/config.service.spec.ts | 1 - src/app/core/integration/integration.service.spec.ts | 2 +- src/app/shared/testing/query-params-directive-stub.ts | 2 +- src/app/shared/testing/utils.ts | 2 +- 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index ce260aefc0..09b3a3b62b 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -25,7 +25,6 @@ export class CommunityPageComponent implements OnInit, OnDestroy { communityRD$: Observable>; logoRD$: Observable>; - private subs: Subscription[] = []; constructor( @@ -42,14 +41,10 @@ export class CommunityPageComponent implements OnInit, OnDestroy { map((rd: RemoteData) => rd.payload), filter((community: Community) => hasValue(community)), mergeMap((community: Community) => community.logo)); - - } ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } - - } diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 8e9f7db27a..44cfdee358 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -36,7 +36,6 @@ describe('ConfigService', () => { const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; - function initTestService(): TestService { return new TestService( requestService, diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index 158f4b0680..152d7ab165 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -40,7 +40,7 @@ describe('IntegrationService', () => { findOptions = new IntegrationSearchOptions(uuid, name, metadata); - function initTestService(): TestService { + function initTestService(): TestService { return new TestService( requestService, halService diff --git a/src/app/shared/testing/query-params-directive-stub.ts b/src/app/shared/testing/query-params-directive-stub.ts index c19c5e6a5f..34216bb53c 100644 --- a/src/app/shared/testing/query-params-directive-stub.ts +++ b/src/app/shared/testing/query-params-directive-stub.ts @@ -6,5 +6,5 @@ import { Directive, Input } from '@angular/core'; selector: '[queryParams]', }) export class QueryParamsDirectiveStub { - @Input('queryParams') queryParams: any; + @Input() queryParams: any; } diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index 8714358100..cd17a1b1f5 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -41,4 +41,4 @@ export function spyOnOperator(obj: any, prop: string): any { }); return spyOn(obj, prop); -} \ No newline at end of file +} From 90b4a0bf2d7d763ace36c7185e0646309950f645 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 20 Dec 2018 14:59:31 +0100 Subject: [PATCH 53/78] 55946: Fixed imports and declarations on ItemCollectionMapperComponent tests --- .../item-collection-mapper.component.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 79bcffe166..5e2b7c4420 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -31,8 +31,13 @@ import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-s import { Observable } from 'rxjs/internal/Observable'; import { of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../../core/cache/response.models'; +import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; +import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { SearchFormComponent } from '../../../shared/search-form/search-form.component'; -describe('ItemCollectionMapperComponent', () => { +fdescribe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; let fixture: ComponentFixture; @@ -87,8 +92,8 @@ describe('ItemCollectionMapperComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [CommonModule, FormsModule, SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], - declarations: [ItemCollectionMapperComponent], + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [ItemCollectionMapperComponent, CollectionSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: Router, useValue: routerStub }, From 1f19324ff6d225f7e08c3ffbd2fa25e7fb9bb881 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 20 Dec 2018 15:00:18 +0100 Subject: [PATCH 54/78] 55946: fdescribe to describe --- .../item-collection-mapper.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 5e2b7c4420..dcc65a41c6 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -37,7 +37,7 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { SearchFormComponent } from '../../../shared/search-form/search-form.component'; -fdescribe('ItemCollectionMapperComponent', () => { +describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; let fixture: ComponentFixture; From f2bfdbcf84216a1a309a2f785b4fb8229ee65929 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 20 Dec 2018 16:11:25 +0100 Subject: [PATCH 55/78] 55946: Prevent list update from calling mapper over and over --- .../item-collection-mapper/item-collection-mapper.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 0b68864bce..53cba34cfb 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -116,11 +116,13 @@ export class ItemCollectionMapperComponent implements OnInit { const itemIdAndExcludingIds$ = observableCombineLatest( this.itemRD$.pipe( getSucceededRemoteData(), + take(1), map((rd: RemoteData) => rd.payload), map((item: Item) => item.id) ), this.itemCollectionsRD$.pipe( getSucceededRemoteData(), + take(1), map((rd: RemoteData>) => rd.payload.page), map((collections: Collection[]) => collections.map((collection: Collection) => collection.id)) ) @@ -168,6 +170,7 @@ export class ItemCollectionMapperComponent implements OnInit { */ private showNotifications(responses$: Observable, messagePrefix: string) { responses$.subscribe((responses: RestResponse[]) => { + console.log('message ' + messagePrefix + ' for ' + responses.length + ' responses...'); const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { From 69f484640748793c0178f12fa1efe731ef8a7a50 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 20 Dec 2018 16:13:26 +0100 Subject: [PATCH 56/78] 55946: Removed console log --- .../item-collection-mapper/item-collection-mapper.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 53cba34cfb..97fbb3a5be 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -170,7 +170,6 @@ export class ItemCollectionMapperComponent implements OnInit { */ private showNotifications(responses$: Observable, messagePrefix: string) { responses$.subscribe((responses: RestResponse[]) => { - console.log('message ' + messagePrefix + ' for ' + responses.length + ' responses...'); const successful = responses.filter((response: RestResponse) => response.isSuccessful); const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { From 13c1a553a1161af52e20f05aa416051fed7bb183 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 21 Dec 2018 13:12:33 +0100 Subject: [PATCH 57/78] 55946: Remove TODOs (fixed) --- .../collection-item-mapper.component.ts | 6 ------ .../item-collection-mapper.component.ts | 4 ---- 2 files changed, 10 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 4d30a761e9..bc205111a4 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -21,9 +21,6 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { isNotEmpty } from '../../shared/empty.util'; import { RestResponse } from '../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Actions, ofType } from '@ngrx/effects'; -import { IndexActionTypes } from '../../core/index/index.actions'; -import { RequestActionTypes } from '../../core/data/request.actions'; @Component({ selector: 'ds-collection-item-mapper', @@ -93,7 +90,6 @@ export class CollectionItemMapperComponent implements OnInit { /** * Load collectionItemsRD$ with a fixed scope to only obtain the items this collection owns * Load mappingItemsRD$ to only obtain items this collection doesn't own - * TODO: When the API support it, fetch items excluding the collection's scope (currently fetches all items) */ loadItemLists() { this.shouldUpdate$ = new BehaviorSubject(true); @@ -197,8 +193,6 @@ export class CollectionItemMapperComponent implements OnInit { * @param event */ tabChange(event) { - // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) - // Temporary solution: Clear url params when changing tabs this.router.navigateByUrl(this.getCurrentUrl()); } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 97fbb3a5be..0eadad860e 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -80,7 +80,6 @@ export class ItemCollectionMapperComponent implements OnInit { /** * Load itemCollectionsRD$ with a fixed scope to only obtain the collections that own this item * Load mappingCollectionsRD$ to only obtain collections that don't own this item - * TODO: When the API support it, fetch collections excluding the item's scope (currently fetches all collections) */ loadCollectionLists() { this.shouldUpdate$ = new BehaviorSubject(true); @@ -142,7 +141,6 @@ export class ItemCollectionMapperComponent implements OnInit { * @param {string[]} ids The list of collection UUID's to remove the mapping of the item for */ removeMappings(ids: string[]) { - // TODO: When the API supports fetching collections excluding the item's scope, make sure to exclude ids from mappingCollectionsRD$ here const responses$ = this.itemRD$.pipe( getSucceededRemoteData(), map((itemRD: RemoteData) => itemRD.payload.id), @@ -212,8 +210,6 @@ export class ItemCollectionMapperComponent implements OnInit { * @param event */ tabChange(event) { - // TODO: Fix tabs to maintain their own pagination options (once the current pagination system is improved) - // Temporary solution: Clear url params when changing tabs this.router.navigateByUrl(this.getCurrentUrl()); } From aeba5d683da09f9a469e76b9d99373dda63ae901 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 31 Dec 2018 10:18:32 +0100 Subject: [PATCH 58/78] 55946: Destroy on hide to prevent pagination issues --- .../collection-item-mapper.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index d094c8e058..29ff2c4e25 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -5,7 +5,7 @@

{{'collection.item-mapper.description' | translate}}

- +
From eb7b4cbf0e6b282230e5d4fbe9f544541cc657b1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 31 Dec 2018 10:19:15 +0100 Subject: [PATCH 59/78] 55946: Destroy on hide to prevent pagination issues 2 --- .../item-collection-mapper.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index fab7a16d79..7386eed98c 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -5,7 +5,7 @@

{{'item.edit.item-mapper.description' | translate}}

- +
From 0b164b33b9bd80dc0a5499785e98d22e994f1698 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 10 Jan 2019 13:14:20 +0100 Subject: [PATCH 60/78] remove unused 'first' imports --- .../search-facet-filter/search-facet-filter.component.ts | 2 +- .../search-filters/search-filter/search-filter.component.ts | 3 +-- src/app/app.component.ts | 2 +- src/app/core/auth/auth.effects.ts | 2 +- src/app/core/auth/auth.service.ts | 1 - src/app/core/auth/server-auth.service.ts | 2 +- src/app/core/cache/builders/remote-data-build.service.ts | 1 - src/app/core/cache/object-cache.service.ts | 2 +- src/app/core/cache/server-sync-buffer.effects.ts | 2 +- src/app/core/data/data.service.ts | 3 +-- src/app/core/metadata/metadata.service.ts | 1 - src/app/core/shared/operators.ts | 2 +- 12 files changed, 9 insertions(+), 14 deletions(-) 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 faaf3b9fb5..fd5a75e7d1 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 @@ -6,7 +6,7 @@ import { Subject, Subscription } from 'rxjs'; -import { switchMap, distinctUntilChanged, first, map, take } from 'rxjs/operators'; +import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index ec239e3628..289b5da143 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -1,5 +1,4 @@ - -import { first, take } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { Component, Input, OnInit } from '@angular/core'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterService } from './search-filter.service'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 98e0d614ae..30a8f01251 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { filter, first, map, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 56a5411ef2..1e68802af8 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,6 +1,6 @@ import { of as observableOf, Observable } from 'rxjs'; -import { filter, debounceTime, switchMap, take, tap, catchError, map, first } from 'rxjs/operators'; +import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; // import @ngrx diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 66fe65a22e..6a2b4afa6e 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -2,7 +2,6 @@ import { Observable, of as observableOf } from 'rxjs'; import { distinctUntilChanged, filter, - first, map, startWith, switchMap, diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 903926fbcf..25ec1156ee 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,4 +1,4 @@ -import { first, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 62a4992787..7561fe3aff 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -7,7 +7,6 @@ import { import { Injectable } from '@angular/core'; import { distinctUntilChanged, - first, flatMap, map, startWith, diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 40f41be14d..af30646f53 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap, take, } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index d0a194705b..0d7392e555 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 6afc84df5a..0921592a83 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,4 +1,4 @@ -import { delay, distinctUntilChanged, filter, find, first, map, take, tap } from 'rxjs/operators'; +import { filter, find, map, take } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -14,7 +14,6 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { compare, Operation } from 'fast-json-patch'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { of } from 'rxjs/internal/observable/of'; export abstract class DataService { protected abstract requestService: RequestService; diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 9a74de992e..136bfe8f3e 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -2,7 +2,6 @@ import { catchError, distinctUntilKeyChanged, filter, - find, first, map, take diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index d9b41ebd73..550ef09163 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { filter, find, first, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; From 04ad79e4169dd5cffded55a39cb2fee8b4477299 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 10 Jan 2019 13:16:27 +0100 Subject: [PATCH 61/78] remove exportToZip config property that was added accidentally --- config/environment.default.js | 1 - src/config/cache-config.interface.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 527e12936e..3c1144fc6f 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -20,7 +20,6 @@ module.exports = { // NOTE: how long should objects be cached for by default msToLive: { default: 15 * 60 * 1000, // 15 minutes - exportToZip: 5 * 1000 // 5 seconds }, // msToLive: 1000, // 15 minutes control: 'max-age=60', // revalidate browser diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index a52eca60e2..ef2d19e76e 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -4,7 +4,6 @@ import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: { default: number; - exportToZip: number; }, control: string, autoSync: AutoSyncConfig From ff924abcfb74482054f3fc19613273a6b3837a3c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 10 Jan 2019 17:06:49 +0100 Subject: [PATCH 62/78] fix a typo in a comment --- .../collection-item-mapper/collection-item-mapper.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index bc205111a4..fb08cfc122 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -33,7 +33,7 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; ] }) /** - * Collection used to map items to a collection + * Component used to map items to a collection */ export class CollectionItemMapperComponent implements OnInit { From 2b1fd76365a53f39d8a7493e2f18cae130c76f42 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 28 May 2019 16:42:02 +0200 Subject: [PATCH 63/78] 62589: Fixed order of imports messing with routes --- src/app/+item-page/item-page.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 123e3ea143..7d947d8805 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -33,11 +33,11 @@ import { RelatedEntitiesSearchComponent } from './simple/related-entities/relate @NgModule({ imports: [ + ItemPageRoutingModule, CommonModule, SharedModule, - EditItemPageModule, - ItemPageRoutingModule, - SearchPageModule + SearchPageModule, + EditItemPageModule ], declarations: [ ItemPageComponent, From 045b87c1c8e41e365d900612178100cd75fcfbdb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 28 May 2019 17:03:57 +0200 Subject: [PATCH 64/78] 62589: Post-Merge Tests and error fixes --- .../collection-item-mapper.component.spec.ts | 9 ++------- .../item-collection-mapper.component.spec.ts | 18 ++++-------------- .../item-select/item-select.component.html | 4 ++-- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 9d4f6e8f7b..d6014f9c3a 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -72,7 +72,7 @@ describe('CollectionItemMapperComponent', () => { paginatedSearchOptions: mockSearchOptions }; const itemDataServiceStub = { - mapToCollection: () => of(new RestResponse(true, '200')) + mapToCollection: () => of(new RestResponse(true, 200, 'OK')) }; const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD }); const translateServiceStub = { @@ -134,11 +134,6 @@ describe('CollectionItemMapperComponent', () => { describe('mapItems', () => { const ids = ['id1', 'id2', 'id3', 'id4']; - beforeEach(() => { - spyOn(notificationsService, 'success').and.callThrough(); - spyOn(notificationsService, 'error').and.callThrough(); - }); - it('should display a success message if at least one mapping was successful', () => { comp.mapItems(ids); expect(notificationsService.success).toHaveBeenCalled(); @@ -146,7 +141,7 @@ describe('CollectionItemMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); comp.mapItems(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index dcc65a41c6..2f04126711 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -69,8 +69,8 @@ describe('ItemCollectionMapperComponent', () => { }; const mockCollectionsRD = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [])); const itemDataServiceStub = { - mapToCollection: () => of(new RestResponse(true, '200')), - removeMappingFromCollection: () => of(new RestResponse(true, '200')), + mapToCollection: () => of(new RestResponse(true, 200, 'OK')), + removeMappingFromCollection: () => of(new RestResponse(true, 200, 'OK')), getMappedCollections: () => of(mockCollectionsRD), /* tslint:disable:no-empty */ clearMappedCollectionsRequests: () => {} @@ -128,11 +128,6 @@ describe('ItemCollectionMapperComponent', () => { describe('mapCollections', () => { const ids = ['id1', 'id2', 'id3', 'id4']; - beforeEach(() => { - spyOn(notificationsService, 'success').and.callThrough(); - spyOn(notificationsService, 'error').and.callThrough(); - }); - it('should display a success message if at least one mapping was successful', () => { comp.mapCollections(ids); expect(notificationsService.success).toHaveBeenCalled(); @@ -140,7 +135,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); comp.mapCollections(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); @@ -150,11 +145,6 @@ describe('ItemCollectionMapperComponent', () => { describe('removeMappings', () => { const ids = ['id1', 'id2', 'id3', 'id4']; - beforeEach(() => { - spyOn(notificationsService, 'success').and.callThrough(); - spyOn(notificationsService, 'error').and.callThrough(); - }); - it('should display a success message if the removal of at least one mapping was successful', () => { comp.removeMappings(ids); expect(notificationsService.success).toHaveBeenCalled(); @@ -162,7 +152,7 @@ describe('ItemCollectionMapperComponent', () => { }); it('should display an error message if the removal of at least one mapping was unsuccessful', () => { - spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(of(new RestResponse(false, '404'))); + spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found'))); comp.removeMappings(ids); expect(notificationsService.success).not.toHaveBeenCalled(); expect(notificationsService.error).toHaveBeenCalled(); diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 522536f86c..51883186e1 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -20,8 +20,8 @@ {{(item.owningCollection | async)?.payload?.name}} - {{item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])[0].value}} - {{item.findMetadata("dc.title")}} + {{item.firstMetadataValue(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])}} + {{item.firstMetadataValue("dc.title")}} From 28fe62f9186d51752102915a4046a50066e47a99 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 29 May 2019 10:01:47 +0200 Subject: [PATCH 65/78] 62589: Fix mappedCollections endpoint --- src/app/core/data/item-data.service.ts | 2 +- .../mapping-collections-reponse-parsing.service.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 71991da780..bf01ce8df8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -82,7 +82,7 @@ export class ItemDataService extends DataService { public getMappingCollectionsEndpoint(itemId: string, collectionId?: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, itemId)), - map((endpoint: string) => `${endpoint}/mappingCollections${collectionId ? `/${collectionId}` : ''}`) + map((endpoint: string) => `${endpoint}/mappedCollections${collectionId ? `/${collectionId}` : ''}`) ); } diff --git a/src/app/core/data/mapping-collections-reponse-parsing.service.ts b/src/app/core/data/mapping-collections-reponse-parsing.service.ts index 9272d3d470..afe9678c7e 100644 --- a/src/app/core/data/mapping-collections-reponse-parsing.service.ts +++ b/src/app/core/data/mapping-collections-reponse-parsing.service.ts @@ -11,16 +11,16 @@ export class MappingCollectionsReponseParsingService implements ResponseParsingS parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; - if (payload._embedded && payload._embedded.mappingCollections) { - const mappingCollections = payload._embedded.mappingCollections; + if (payload._embedded && payload._embedded.mappedCollections) { + const mappedCollections = payload._embedded.mappedCollections; // TODO: When the API supports it, change this to fetch a paginated list, instead of creating static one // Reason: Pagination is currently not supported on the mappingCollections endpoint const paginatedMappingCollections = new PaginatedList(Object.assign(new PageInfo(), { - elementsPerPage: mappingCollections.length, - totalElements: mappingCollections.length, + elementsPerPage: mappedCollections.length, + totalElements: mappedCollections.length, totalPages: 1, currentPage: 1 - }), mappingCollections); + }), mappedCollections); return new GenericSuccessResponse(paginatedMappingCollections, data.statusCode, data.statusText); } else { return new ErrorResponse( From df730efbd0b2f2ce53e2c87824e99489fb8a93ec Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 29 May 2019 11:44:34 +0200 Subject: [PATCH 66/78] 62589: Provider and POST item-collection mapping fix --- .../collection-item-mapper.component.ts | 17 ++++++++++++----- .../collection-page.module.ts | 4 +++- src/app/core/data/item-data.service.ts | 19 +++++++++++++------ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index fb08cfc122..520c446d8e 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; @@ -21,6 +21,7 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { isNotEmpty } from '../../shared/empty.util'; import { RestResponse } from '../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; @Component({ selector: 'ds-collection-item-mapper', @@ -30,6 +31,12 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; animations: [ fadeIn, fadeInOut + ], + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } ] }) /** @@ -73,7 +80,7 @@ export class CollectionItemMapperComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, - private searchConfigService: SearchConfigurationService, + @Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService, private searchService: SearchService, private notificationsService: NotificationsService, private itemDataService: ItemDataService, @@ -130,10 +137,10 @@ export class CollectionItemMapperComponent implements OnInit { mapItems(ids: string[], remove?: boolean) { const responses$ = this.collectionRD$.pipe( getSucceededRemoteData(), - map((collectionRD: RemoteData) => collectionRD.payload.id), - switchMap((collectionId: string) => + map((collectionRD: RemoteData) => collectionRD.payload), + switchMap((collection: Collection) => observableCombineLatest(ids.map((id: string) => - remove ? this.itemDataService.removeMappingFromCollection(id, collectionId) : this.itemDataService.mapToCollection(id, collectionId) + remove ? this.itemDataService.removeMappingFromCollection(id, collection.id) : this.itemDataService.mapToCollection(id, collection.self) )) ) ); diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index 86afb37170..0eaeca8ca7 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -11,6 +11,7 @@ import { EditCollectionPageComponent } from './edit-collection-page/edit-collect import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { SearchService } from '../+search-page/search-service/search.service'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; +import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service'; @NgModule({ imports: [ @@ -27,7 +28,8 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti CollectionItemMapperComponent ], providers: [ - SearchService + SearchService, + SearchFixedFilterService ] }) export class CollectionPageModule { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index bf01ce8df8..f26f0574f7 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -23,7 +23,7 @@ import { 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, @@ -36,6 +36,7 @@ import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; import { Collection } from '../shared/collection.model'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @Injectable() export class ItemDataService extends DataService { @@ -104,14 +105,20 @@ export class ItemDataService extends DataService { /** * Maps an item to a collection - * @param itemId The item's id - * @param collectionId The collection's id + * @param itemId The item's id + * @param collectionHref The collection's self link */ - public mapToCollection(itemId: string, collectionId: string): Observable { - return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + public mapToCollection(itemId: string, collectionHref: string): Observable { + return this.getMappingCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), + map((endpointURL: string) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); + }), configureRequest(this.requestService), switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)), getResponseFromEntry() From acf83f62618180d7c0214c093324f4e38cd600c2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 29 May 2019 12:03:27 +0200 Subject: [PATCH 67/78] 62589: Test provider fix --- .../collection-item-mapper.component.spec.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index d6014f9c3a..4100c49d9e 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -37,8 +37,10 @@ import { ObjectSelectService } from '../../shared/object-select/object-select.se import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service-stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { Observable } from 'rxjs/internal/Observable'; -import { of } from 'rxjs/internal/observable/of'; +import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../core/cache/response.models'; +import { RouteService } from '../../shared/services/route.service'; +import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -94,6 +96,22 @@ describe('CollectionItemMapperComponent', () => { clearMappingItemsRequests: () => {} /* tslint:enable:no-empty */ }; + const routeServiceStub = { + getRouteParameterValue: () => { + return observableOf(''); + }, + getQueryParameterValue: () => { + return observableOf('') + }, + getQueryParamsWithPrefix: () => { + return observableOf('') + } + }; + const fixedFilterServiceStub = { + getQueryByFilterName: () => { + return observableOf('') + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -109,7 +127,9 @@ describe('CollectionItemMapperComponent', () => { { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: TranslateService, useValue: translateServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() } + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: SearchFixedFilterService, useValue: fixedFilterServiceStub } ] }).compileComponents(); })); From 26e25069ad46c70ed5e8383f198c255c59886de1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 12 Aug 2019 16:41:56 +0200 Subject: [PATCH 68/78] 62589: PR Feedback --- src/app/core/core.module.ts | 4 ++-- src/app/core/data/item-data.service.ts | 4 ++-- ....ts => mapped-collections-reponse-parsing.service.ts} | 6 +++++- src/app/core/data/request.models.ts | 9 ++++++--- 4 files changed, 15 insertions(+), 8 deletions(-) rename src/app/core/data/{mapping-collections-reponse-parsing.service.ts => mapped-collections-reponse-parsing.service.ts} (84%) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 00f160d5ef..08f09d99cc 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -117,7 +117,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model'; import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model'; import { BrowseDefinition } from './shared/browse-definition.model'; -import { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service'; +import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service'; const IMPORTS = [ @@ -165,7 +165,7 @@ const PROVIDERS = [ RegistryMetadataschemasResponseParsingService, RegistryMetadatafieldsResponseParsingService, RegistryBitstreamformatsResponseParsingService, - MappingCollectionsReponseParsingService, + MappedCollectionsReponseParsingService, DebugResponseParsingService, SearchResponseParsingService, MyDSpaceResponseParsingService, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f26f0574f7..94c77664d3 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -15,7 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DeleteRequest, FindAllOptions, - MappingCollectionsRequest, + MappedCollectionsRequest, PatchRequest, PostRequest, RestRequest @@ -133,7 +133,7 @@ export class ItemDataService extends DataService { const request$ = this.getMappingCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpointURL: string) => new MappingCollectionsRequest(this.requestService.generateRequestId(), endpointURL)), + map((endpointURL: string) => new MappedCollectionsRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService) ); diff --git a/src/app/core/data/mapping-collections-reponse-parsing.service.ts b/src/app/core/data/mapped-collections-reponse-parsing.service.ts similarity index 84% rename from src/app/core/data/mapping-collections-reponse-parsing.service.ts rename to src/app/core/data/mapped-collections-reponse-parsing.service.ts index afe9678c7e..45dd361b23 100644 --- a/src/app/core/data/mapping-collections-reponse-parsing.service.ts +++ b/src/app/core/data/mapped-collections-reponse-parsing.service.ts @@ -7,7 +7,11 @@ import { PageInfo } from '../shared/page-info.model'; import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; @Injectable() -export class MappingCollectionsReponseParsingService implements ResponseParsingService { +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse + * containing a PaginatedList of mapped collections + */ +export class MappedCollectionsReponseParsingService implements ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ac2e17a727..b327306fcb 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -18,7 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; -import { MappingCollectionsReponseParsingService } from './mapping-collections-reponse-parsing.service'; +import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -186,9 +186,12 @@ export class BrowseItemsRequest extends GetRequest { } } -export class MappingCollectionsRequest extends GetRequest { +/** + * Request to fetch the mapped collections of an item + */ +export class MappedCollectionsRequest extends GetRequest { getResponseParser(): GenericConstructor { - return MappingCollectionsReponseParsingService; + return MappedCollectionsReponseParsingService; } } From c95fa8fb96182fe8146523f07193cb655b1492b1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 19 Aug 2019 14:18:10 +0200 Subject: [PATCH 69/78] 62589: Refactoring mapping endpoint and methods to mapped + collection-list sorting bugfix --- .../collection-item-mapper.component.html | 2 +- .../collection-item-mapper.component.spec.ts | 2 +- .../collection-item-mapper.component.ts | 8 ++++---- .../item-collection-mapper.component.html | 3 ++- .../item-collection-mapper.component.ts | 6 +++--- src/app/core/data/collection-data.service.ts | 12 ++++++------ src/app/core/data/item-data.service.ts | 12 ++++++------ .../mapped-collections-reponse-parsing.service.ts | 8 ++++---- .../collection-select.component.html | 1 + .../item-select/item-select.component.html | 1 + .../object-select/object-select.component.ts | 7 +++++++ 11 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 29ff2c4e25..9b7216c92a 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -34,7 +34,7 @@
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 4100c49d9e..046127da4c 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -93,7 +93,7 @@ describe('CollectionItemMapperComponent', () => { const collectionDataServiceStub = { getMappedItems: () => of(emptyList), /* tslint:disable:no-empty */ - clearMappingItemsRequests: () => {} + clearMappedItemsRequests: () => {} /* tslint:enable:no-empty */ }; const routeServiceStub = { diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 520c446d8e..554696c63f 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -64,7 +64,7 @@ export class CollectionItemMapperComponent implements OnInit { * List of items to show under the "Map" tab * Items outside the collection */ - mappingItemsRD$: Observable>>; + mappedItemsRD$: Observable>>; /** * Sort on title ASC by default @@ -96,7 +96,7 @@ export class CollectionItemMapperComponent implements OnInit { /** * Load collectionItemsRD$ with a fixed scope to only obtain the items this collection owns - * Load mappingItemsRD$ to only obtain items this collection doesn't own + * Load mappedItemsRD$ to only obtain items this collection doesn't own */ loadItemLists() { this.shouldUpdate$ = new BehaviorSubject(true); @@ -114,7 +114,7 @@ export class CollectionItemMapperComponent implements OnInit { } }) ); - this.mappingItemsRD$ = collectionAndOptions$.pipe( + this.mappedItemsRD$ = collectionAndOptions$.pipe( switchMap(([collectionRD, options, shouldUpdate]) => { if (shouldUpdate) { return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), { @@ -190,7 +190,7 @@ export class CollectionItemMapperComponent implements OnInit { */ private clearRequestCache() { this.collectionRD$.pipe(take(1)).subscribe((collectionRD: RemoteData) => { - this.collectionDataService.clearMappingItemsRequests(collectionRD.payload.id); + this.collectionDataService.clearMappedItemsRequests(collectionRD.payload.id); this.searchService.clearDiscoveryRequests(); }); } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 7386eed98c..55619bdc96 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -32,8 +32,9 @@
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 0eadad860e..803083c428 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -54,7 +54,7 @@ export class ItemCollectionMapperComponent implements OnInit { * List of collections to show under the "Map" tab * Collections that are not mapped to the item */ - mappingCollectionsRD$: Observable>>; + mappedCollectionsRD$: Observable>>; /** * Firing this observable (shouldUpdate$.next(true)) forces the two lists to reload themselves @@ -79,7 +79,7 @@ export class ItemCollectionMapperComponent implements OnInit { /** * Load itemCollectionsRD$ with a fixed scope to only obtain the collections that own this item - * Load mappingCollectionsRD$ to only obtain collections that don't own this item + * Load mappedCollectionsRD$ to only obtain collections that don't own this item */ loadCollectionLists() { this.shouldUpdate$ = new BehaviorSubject(true); @@ -96,7 +96,7 @@ export class ItemCollectionMapperComponent implements OnInit { this.itemCollectionsRD$, this.searchOptions$ ); - this.mappingCollectionsRD$ = itemCollectionsAndOptions$.pipe( + this.mappedCollectionsRD$ = itemCollectionsAndOptions$.pipe( switchMap(([itemCollectionsRD, searchOptions]) => { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { query: this.buildQuery(itemCollectionsRD.payload.page, searchOptions.query), diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index e4b8442718..38b86b3817 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -69,10 +69,10 @@ export class CollectionDataService extends ComColDataService { * Fetches the endpoint used for mapping items to a collection * @param collectionId The id of the collection to map items to */ - getMappingItemsEndpoint(collectionId): Observable { + getMappedItemsEndpoint(collectionId): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, collectionId)), - map((endpoint: string) => `${endpoint}/mappingItems`) + map((endpoint: string) => `${endpoint}/mappedItems`) ); } @@ -84,7 +84,7 @@ export class CollectionDataService extends ComColDataService { getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions): Observable>> { const requestUuid = this.requestService.generateRequestId(); - const href$ = this.getMappingItemsEndpoint(collectionId).pipe( + const href$ = this.getMappedItemsEndpoint(collectionId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) @@ -106,11 +106,11 @@ export class CollectionDataService extends ComColDataService { } /** - * Clears all requests (from cache) connected to the mappingItems endpoint + * Clears all requests (from cache) connected to the mappedItems endpoint * @param collectionId */ - clearMappingItemsRequests(collectionId: string) { - this.getMappingItemsEndpoint(collectionId).pipe(take(1)).subscribe((href: string) => { + clearMappedItemsRequests(collectionId: string) { + this.getMappedItemsEndpoint(collectionId).pipe(take(1)).subscribe((href: string) => { this.requestService.removeByHrefSubstring(href); }); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 94c77664d3..9b59307f34 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -80,7 +80,7 @@ export class ItemDataService extends DataService { * @param itemId The item's id * @param collectionId The collection's id (optional) */ - public getMappingCollectionsEndpoint(itemId: string, collectionId?: string): Observable { + public getMappedCollectionsEndpoint(itemId: string, collectionId?: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, itemId)), map((endpoint: string) => `${endpoint}/mappedCollections${collectionId ? `/${collectionId}` : ''}`) @@ -93,7 +93,7 @@ export class ItemDataService extends DataService { * @param collectionId The collection's id */ public removeMappingFromCollection(itemId: string, collectionId: string): Observable { - return this.getMappingCollectionsEndpoint(itemId, collectionId).pipe( + return this.getMappedCollectionsEndpoint(itemId, collectionId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), @@ -109,7 +109,7 @@ export class ItemDataService extends DataService { * @param collectionHref The collection's self link */ public mapToCollection(itemId: string, collectionHref: string): Observable { - return this.getMappingCollectionsEndpoint(itemId).pipe( + return this.getMappedCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => { @@ -130,7 +130,7 @@ export class ItemDataService extends DataService { * @param itemId The item's id */ public getMappedCollections(itemId: string): Observable>> { - const request$ = this.getMappingCollectionsEndpoint(itemId).pipe( + const request$ = this.getMappedCollectionsEndpoint(itemId).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => new MappedCollectionsRequest(this.requestService.generateRequestId(), endpointURL)), @@ -149,11 +149,11 @@ export class ItemDataService extends DataService { } /** - * Clears all requests (from cache) connected to the mappingCollections endpoint + * Clears all requests (from cache) connected to the mappedCollections endpoint * @param itemId */ public clearMappedCollectionsRequests(itemId: string) { - this.getMappingCollectionsEndpoint(itemId).pipe(take(1)).subscribe((href: string) => { + this.getMappedCollectionsEndpoint(itemId).pipe(take(1)).subscribe((href: string) => { this.requestService.removeByHrefSubstring(href); }); } diff --git a/src/app/core/data/mapped-collections-reponse-parsing.service.ts b/src/app/core/data/mapped-collections-reponse-parsing.service.ts index 45dd361b23..bf8ed036e3 100644 --- a/src/app/core/data/mapped-collections-reponse-parsing.service.ts +++ b/src/app/core/data/mapped-collections-reponse-parsing.service.ts @@ -18,18 +18,18 @@ export class MappedCollectionsReponseParsingService implements ResponseParsingSe if (payload._embedded && payload._embedded.mappedCollections) { const mappedCollections = payload._embedded.mappedCollections; // TODO: When the API supports it, change this to fetch a paginated list, instead of creating static one - // Reason: Pagination is currently not supported on the mappingCollections endpoint - const paginatedMappingCollections = new PaginatedList(Object.assign(new PageInfo(), { + // Reason: Pagination is currently not supported on the mappedCollections endpoint + const paginatedMappedCollections = new PaginatedList(Object.assign(new PageInfo(), { elementsPerPage: mappedCollections.length, totalElements: mappedCollections.length, totalPages: 1, currentPage: 1 }), mappedCollections); - return new GenericSuccessResponse(paginatedMappingCollections, data.statusCode, data.statusText); + return new GenericSuccessResponse(paginatedMappedCollections, data.statusCode, data.statusText); } else { return new ErrorResponse( Object.assign( - new Error('Unexpected response from mappingCollections endpoint'), data + new Error('Unexpected response from mappedCollections endpoint'), data ) ); } diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index d53a030baf..5a1c98fcf5 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -2,6 +2,7 @@ implements OnInit, OnDestro @Input() paginationOptions: PaginationComponentOptions; + /** + * The sorting options used to display the DSpaceObjects + */ + @Input() + sortOptions: SortOptions; + /** * The message key used for the confirm button * @type {string} From 02b007a0f6d246fee831f851d8feb78934e322ed Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 10 Sep 2019 16:29:15 +0200 Subject: [PATCH 70/78] 62589: Exclude owning collection + redirect to first tab after mapping + page reload fix --- .../collection-item-mapper.component.html | 6 ++--- .../collection-item-mapper.component.ts | 18 +++++++++++-- .../item-collection-mapper.component.html | 6 ++--- .../item-collection-mapper.component.ts | 27 ++++++++++++++++--- .../collection-select.component.html | 2 +- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 9b7216c92a..9e9f9fd8e1 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -5,8 +5,8 @@

{{'collection.item-mapper.description' | translate}}

- - + +
- +
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 554696c63f..059bd098d4 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild } from '@angular/core'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; @@ -44,6 +44,12 @@ import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.comp */ export class CollectionItemMapperComponent implements OnInit { + /** + * A view on the tabset element + * Used to switch tabs programmatically + */ + @ViewChild('tabs') tabs; + /** * The collection to map items to */ @@ -180,8 +186,9 @@ export class CollectionItemMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } - // Force an update on all lists + // Force an update on all lists and switch back to the first tab this.shouldUpdate$.next(true); + this.switchToFirstTab(); }); } @@ -228,4 +235,11 @@ export class CollectionItemMapperComponent implements OnInit { } } + /** + * Switch the view to focus on the first tab + */ + switchToFirstTab() { + this.tabs.select('browseTab'); + } + } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 55619bdc96..97cbb27871 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -5,8 +5,8 @@

{{'item.edit.item-mapper.description' | translate}}

- - + +
- +
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 803083c428..3a85a75659 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { RemoteData } from '../../../core/data/remote-data'; @@ -34,6 +34,13 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; * Component for mapping collections to an item */ export class ItemCollectionMapperComponent implements OnInit { + + /** + * A view on the tabset element + * Used to switch tabs programmatically + */ + @ViewChild('tabs') tabs; + /** * The item to map to collections */ @@ -92,14 +99,18 @@ export class ItemCollectionMapperComponent implements OnInit { switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id)) ); + const owningCollectionRD$ = this.itemRD$.pipe( + switchMap((itemRD: RemoteData) => itemRD.payload.owningCollection) + ); const itemCollectionsAndOptions$ = observableCombineLatest( this.itemCollectionsRD$, + owningCollectionRD$, this.searchOptions$ ); this.mappedCollectionsRD$ = itemCollectionsAndOptions$.pipe( - switchMap(([itemCollectionsRD, searchOptions]) => { + switchMap(([itemCollectionsRD, owningCollectionRD, searchOptions]) => { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { - query: this.buildQuery(itemCollectionsRD.payload.page, searchOptions.query), + query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query), dsoType: DSpaceObjectType.COLLECTION })); }), @@ -190,8 +201,9 @@ export class ItemCollectionMapperComponent implements OnInit { this.notificationsService.error(head, content); }); } - // Force an update on all lists + // Force an update on all lists and switch back to the first tab this.shouldUpdate$.next(true); + this.switchToFirstTab(); }); } @@ -251,4 +263,11 @@ export class ItemCollectionMapperComponent implements OnInit { } } + /** + * Switch the view to focus on the first tab + */ + switchToFirstTab() { + this.tabs.select('browseTab'); + } + } diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index 5a1c98fcf5..e7c20d268c 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -1,6 +1,6 @@ Date: Wed, 11 Sep 2019 10:44:03 +0200 Subject: [PATCH 71/78] 62589: Fixed import --- .../collection-item-mapper.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 046127da4c..dcbce697c6 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -39,8 +39,8 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../core/cache/response.models'; -import { RouteService } from '../../shared/services/route.service'; import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service'; +import { RouteService } from '../../core/services/route.service'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; From 647bfb4e0d859a54344fcf1cc5d2475203e2cf06 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 16 Sep 2019 14:06:54 +0200 Subject: [PATCH 72/78] 62589: Import fix --- src/app/core/data/item-data.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 9f2621d124..cec60f3305 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -17,7 +17,7 @@ import { FindAllOptions, MappedCollectionsRequest, PatchRequest, - PostRequest, + PostRequest, PutRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; From 145ed346c85c4ae13b26e834f9c17febc5c6cea7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 27 Sep 2019 16:40:47 +0200 Subject: [PATCH 73/78] 62589: Feedback improvements and fixes --- resources/i18n/en.json5 | 4 +-- .../collection-item-mapper.component.html | 14 +++++---- .../collection-item-mapper.component.ts | 15 +++++++++- .../item-collection-mapper.component.html | 14 +++++---- .../item-collection-mapper.component.ts | 15 +++++++++- src/app/+item-page/item-page.module.ts | 2 +- .../collection-select.component.html | 6 +++- .../item-select/item-select.component.html | 6 +++- .../item-select/item-select.component.ts | 8 ++++- .../object-select/object-select.component.ts | 29 ++++++++++++++++++- 10 files changed, 94 insertions(+), 19 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index d9e380d198..ba3d1549c1 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -139,6 +139,7 @@ "collection.form.tableofcontents": "News (HTML)", "collection.form.title": "Name", + "collection.item-mapper.cancel": "Cancel", "collection.item-mapper.collection": "Collection: \"{{name}}\"", "collection.item-mapper.confirm": "Map selected items", "collection.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", @@ -152,7 +153,6 @@ "collection.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", "collection.item-mapper.notifications.unmap.success.head": "Remove mapping completed", "collection.item-mapper.remove": "Remove selected item mappings", - "collection.item-mapper.return": "Return", "collection.item-mapper.tabs.browse": "Browse", "collection.item-mapper.tabs.map": "Map", @@ -252,6 +252,7 @@ "item.edit.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", + "item.edit.item-mapper.cancel": "Cancel", "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.item": "Item: \"{{name}}\"", @@ -263,7 +264,6 @@ "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - "item.edit.item-mapper.return": "Return", "item.edit.item-mapper.tabs.browse": "Browse", "item.edit.item-mapper.tabs.map": "Map", diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 9e9f9fd8e1..23e23b5c25 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -14,8 +14,11 @@ [dsoRD$]="collectionItemsRD$" [paginationOptions]="(searchOptions$ | async)?.pagination" [confirmButton]="'collection.item-mapper.remove'" + [cancelButton]="'collection.item-mapper.cancel'" + [dangerConfirm]="true" [hideCollection]="true" - (confirm)="mapItems($event, true)"> + (confirm)="mapItems($event, true)" + (cancel)="onCancel()">
@@ -26,7 +29,8 @@ + [currentUrl]="'./'" + [inPlaceSearch]="true">
@@ -37,13 +41,13 @@ [dsoRD$]="mappedItemsRD$" [paginationOptions]="(searchOptions$ | async)?.pagination" [confirmButton]="'collection.item-mapper.confirm'" - (confirm)="mapItems($event)"> + [cancelButton]="'collection.item-mapper.cancel'" + (confirm)="mapItems($event)" + (cancel)="onCancel()">
- -
diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 059bd098d4..417b044277 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -9,7 +9,7 @@ import { SearchConfigurationService } from '../../+search-page/search-service/se import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; import { map, switchMap, take, tap } from 'rxjs/operators'; -import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; +import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; @@ -242,4 +242,17 @@ export class CollectionItemMapperComponent implements OnInit { this.tabs.select('browseTab'); } + /** + * When a cancel event is fired, return to the collection page + */ + onCancel() { + this.collectionRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + take(1) + ).subscribe((collection: Collection) => { + this.router.navigate(['/collections/', collection.id]) + }); + } + } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 97cbb27871..d4433cd4a2 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -14,7 +14,10 @@ [dsoRD$]="itemCollectionsRD$" [paginationOptions]="(searchOptions$ | async)?.pagination" [confirmButton]="'item.edit.item-mapper.buttons.remove'" - (confirm)="removeMappings($event)"> + [cancelButton]="'item.edit.item-mapper.cancel'" + [dangerConfirm]="true" + (confirm)="removeMappings($event)" + (cancel)="onCancel()">
@@ -24,7 +27,8 @@
+ [currentUrl]="'./'" + [inPlaceSearch]="true">
@@ -36,13 +40,13 @@ [paginationOptions]="(searchOptions$ | async)?.pagination" [sortOptions]="(searchOptions$ | async)?.sort" [confirmButton]="'item.edit.item-mapper.buttons.add'" - (confirm)="mapCollections($event)">
+ [cancelButton]="'item.edit.item-mapper.cancel'" + (confirm)="mapCollections($event)" + (cancel)="onCancel()">
- -
diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 3a85a75659..b8073edb39 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -7,7 +7,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; -import { getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; +import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; @@ -270,4 +270,17 @@ export class ItemCollectionMapperComponent implements OnInit { this.tabs.select('browseTab'); } + /** + * When a cancel event is fired, return to the item page + */ + onCancel() { + this.itemRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + take(1) + ).subscribe((item: Item) => { + this.router.navigate(['/items/', item.id]) + }); + } + } diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index f510ccf19b..2a5d0b6da7 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -31,8 +31,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field imports: [ CommonModule, SharedModule, - EditItemPageModule, ItemPageRoutingModule, + EditItemPageModule, SearchPageModule ], declarations: [ diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index e7c20d268c..3e2ecdaebf 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -24,5 +24,9 @@
- +
+ + + +
diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index a54afed643..5dbc88e55d 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -28,5 +28,9 @@
- +
+ + + +
diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index 2cd5b502df..7dd8239960 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -1,4 +1,4 @@ -import { Component} from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { ObjectSelectService } from '../object-select.service'; import { ObjectSelectComponent } from '../object-select/object-select.component'; @@ -14,6 +14,12 @@ import { isNotEmpty } from '../../empty.util'; */ export class ItemSelectComponent extends ObjectSelectComponent { + /** + * Whether or not to hide the collection column + */ + @Input() + hideCollection = false; + constructor(protected objectSelectService: ObjectSelectService) { super(objectSelectService); } diff --git a/src/app/shared/object-select/object-select/object-select.component.ts b/src/app/shared/object-select/object-select/object-select.component.ts index 70099e7328..e3a50b8024 100644 --- a/src/app/shared/object-select/object-select/object-select.component.ts +++ b/src/app/shared/object-select/object-select/object-select.component.ts @@ -12,6 +12,9 @@ import { SortOptions } from '../../../core/cache/models/sort-options.model'; */ export abstract class ObjectSelectComponent implements OnInit, OnDestroy { + /** + * A unique key used for the object select service + */ @Input() key: string; @@ -40,8 +43,18 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro @Input() confirmButton: string; + /** + * The message key used for the cancel button + * @type {string} + */ @Input() - hideCollection = false; + cancelButton: string; + + /** + * An event fired when the cancel button is clicked + */ + @Output() + cancel = new EventEmitter(); /** * EventEmitter to return the selected UUIDs when the confirm button is pressed @@ -50,6 +63,13 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro @Output() confirm: EventEmitter = new EventEmitter(); + /** + * Whether or not to render the confirm button as danger (for example if confirm deletes objects) + * Defaults to false + */ + @Input() + dangerConfirm = false; + /** * The list of selected UUIDs */ @@ -96,4 +116,11 @@ export abstract class ObjectSelectComponent implements OnInit, OnDestro }); } + /** + * Fire a cancel event + */ + onCancel() { + this.cancel.emit(); + } + } From a2fb8a316b3c47b85a995db892839c54907738c0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 30 Sep 2019 10:35:52 +0200 Subject: [PATCH 74/78] 62589: Added tests for more coverage --- .../collection-item-mapper.component.spec.ts | 43 ++++++++++++++- .../item-collection-mapper.component.spec.ts | 45 +++++++++++++++- .../core/data/collection-data.service.spec.ts | 54 +++++++++++++++++++ src/app/core/data/item-data.service.spec.ts | 49 +++++++++++++++-- .../collection-select.component.spec.ts | 14 +++++ .../item-select/item-select.component.spec.ts | 14 +++++ 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 src/app/core/data/collection-data.service.spec.ts diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index dcbce697c6..8332d20cea 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -67,8 +67,12 @@ describe('CollectionItemMapperComponent', () => { sort: new SortOptions('dc.title', SortDirection.ASC), scope: mockCollection.id })); + const url = 'http://test.url'; + const urlWithParam = url + '?param=value'; const routerStub = Object.assign(new RouterStub(), { - url: 'http://test.url' + url: urlWithParam, + navigateByUrl: {}, + navigate: {} }); const searchConfigServiceStub = { paginatedSearchOptions: mockSearchOptions @@ -168,4 +172,41 @@ describe('CollectionItemMapperComponent', () => { }); }); + describe('tabChange', () => { + beforeEach(() => { + spyOn(routerStub, 'navigateByUrl'); + comp.tabChange({}); + }); + + it('should navigate to the same page to remove parameters', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith(url); + }); + }); + + describe('buildQuery', () => { + const query = 'query'; + const expected = `-location.coll:\"${mockCollection.id}\" AND ${query}`; + + let result; + + beforeEach(() => { + result = comp.buildQuery(mockCollection.id, query); + }); + + it('should build a solr query to exclude the provided collection', () => { + expect(result).toEqual(expected); + }) + }); + + describe('onCancel', () => { + beforeEach(() => { + spyOn(routerStub, 'navigate'); + comp.onCancel(); + }); + + it('should navigate to the collection page', () => { + expect(router.navigate).toHaveBeenCalledWith(['/collections/', mockCollection.id]); + }); + }); + }); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 2f04126711..018ed3f2ac 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -36,6 +36,7 @@ import { PaginationComponent } from '../../../shared/pagination/pagination.compo import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { SearchFormComponent } from '../../../shared/search-form/search-form.component'; +import { Collection } from '../../../core/shared/collection.model'; describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; @@ -48,6 +49,7 @@ describe('ItemCollectionMapperComponent', () => { let notificationsService: NotificationsService; let itemDataService: ItemDataService; + const mockCollection = Object.assign(new Collection(), { id: 'collection1' }); const mockItem: Item = Object.assign(new Item(), { id: '932c7d50-d85a-44cb-b9dc-b427b12877bd', name: 'test-item' @@ -61,8 +63,12 @@ describe('ItemCollectionMapperComponent', () => { }), sort: new SortOptions('dc.title', SortDirection.ASC) })); + const url = 'http://test.url'; + const urlWithParam = url + '?param=value'; const routerStub = Object.assign(new RouterStub(), { - url: 'http://test.url' + url: urlWithParam, + navigateByUrl: {}, + navigate: {} }); const searchConfigServiceStub = { paginatedSearchOptions: mockSearchOptions @@ -159,4 +165,41 @@ describe('ItemCollectionMapperComponent', () => { }); }); + describe('tabChange', () => { + beforeEach(() => { + spyOn(routerStub, 'navigateByUrl'); + comp.tabChange({}); + }); + + it('should navigate to the same page to remove parameters', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith(url); + }); + }); + + describe('buildQuery', () => { + const query = 'query'; + const expected = `${query} AND -search.resourceid:${mockCollection.id}`; + + let result; + + beforeEach(() => { + result = comp.buildQuery([mockCollection], query); + }); + + it('should build a solr query to exclude the provided collection', () => { + expect(result).toEqual(expected); + }) + }); + + describe('onCancel', () => { + beforeEach(() => { + spyOn(routerStub, 'navigate'); + comp.onCancel(); + }); + + it('should navigate to the item page', () => { + expect(router.navigate).toHaveBeenCalledWith(['/items/', mockItem.id]); + }); + }); + }); diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts new file mode 100644 index 0000000000..b0b3889c9c --- /dev/null +++ b/src/app/core/data/collection-data.service.spec.ts @@ -0,0 +1,54 @@ +import { CollectionDataService } from './collection-data.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestService } from './request.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GetRequest } from './request.models'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; + +describe('CollectionDataService', () => { + let service: CollectionDataService; + let objectCache: ObjectCacheService; + let requestService: RequestService; + let halService: HALEndpointService; + let rdbService: RemoteDataBuildService; + + const url = 'fake-collections-url'; + + beforeEach(() => { + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + requestService = getMockRequestService(); + halService = Object.assign(new HALEndpointServiceStub(url)); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: jasmine.createSpy('buildList') + }); + + service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, null, null, null); + }); + + describe('getMappedItems', () => { + let result; + + beforeEach(() => { + result = service.getMappedItems('collection-id'); + }); + + it('should configure a GET request', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest), undefined); + }); + }); + + describe('clearMappedItemsRequests', () => { + beforeEach(() => { + service.clearMappedItemsRequests('collection-id'); + }); + + it('should remote request cache', () => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalled(); + }); + }); + +}); diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 3553a63af4..36b8e6b3c5 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -7,7 +7,14 @@ import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindAllOptions, RestRequest } from './request.models'; +import { + DeleteRequest, + FindAllOptions, + GetRequest, + MappedCollectionsRequest, + PostRequest, + RestRequest +} from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Observable } from 'rxjs'; import { RestResponse } from '../cache/response.models'; @@ -16,12 +23,13 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { HttpClient } from '@angular/common/http'; import { RequestEntry } from './request.reducer'; import { of as observableOf } from 'rxjs'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; let service: ItemDataService; let bs: BrowseService; - const requestService = { + const requestService = Object.assign(getMockRequestService(), { generateRequestId(): string { return scopeID; }, @@ -32,9 +40,14 @@ describe('ItemDataService', () => { const responseCacheEntry = new RequestEntry(); responseCacheEntry.response = new RestResponse(true, 200, 'OK'); return observableOf(responseCacheEntry); + }, + removeByHrefSubstring(href: string) { + // Do nothing } - } as RequestService; - const rdbService = {} as RemoteDataBuildService; + }) as RequestService; + const rdbService = jasmine.createSpyObj('rdbService', { + toRemoteDataObservable: observableOf({}) + }); const store = {} as Store; const objectCache = {} as ObjectCacheService; @@ -162,4 +175,32 @@ describe('ItemDataService', () => { }); }); + describe('removeMappingFromCollection', () => { + let result; + + beforeEach(() => { + service = initTestService(); + spyOn(requestService, 'configure'); + result = service.removeMappingFromCollection('item-id', 'collection-id'); + }); + + it('should configure a DELETE request', () => { + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest), undefined)); + }); + }); + + describe('mapToCollection', () => { + let result; + + beforeEach(() => { + service = initTestService(); + spyOn(requestService, 'configure'); + result = service.mapToCollection('item-id', 'collection-href'); + }); + + it('should configure a POST request', () => { + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest), undefined)); + }); + }); + }); diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index bc83c3d52a..c9f79f6af5 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -101,4 +101,18 @@ describe('ItemSelectComponent', () => { expect(comp.confirm.emit).toHaveBeenCalled(); }); }); + + describe('when cancel is clicked', () => { + let cancelButton: HTMLButtonElement; + + beforeEach(() => { + cancelButton = fixture.debugElement.query(By.css('button.collection-cancel')).nativeElement; + spyOn(comp.cancel, 'emit').and.callThrough(); + }); + + it('should emit a cancel event',() => { + cancelButton.click(); + expect(comp.cancel.emit).toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index be7c315c45..33fa4dcd7e 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -123,4 +123,18 @@ describe('ItemSelectComponent', () => { expect(comp.confirm.emit).toHaveBeenCalled(); }); }); + + describe('when cancel is clicked', () => { + let cancelButton: HTMLButtonElement; + + beforeEach(() => { + cancelButton = fixture.debugElement.query(By.css('button.item-cancel')).nativeElement; + spyOn(comp.cancel, 'emit').and.callThrough(); + }); + + it('should emit a cancel event',() => { + cancelButton.click(); + expect(comp.cancel.emit).toHaveBeenCalled(); + }); + }); }); From b8a466d12b5f6cdf8e60b425667a923f0d847ba8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 30 Sep 2019 16:17:18 +0200 Subject: [PATCH 75/78] 62589: Loading components for item and collection select lists --- resources/i18n/en.json5 | 4 ++++ .../collection-item-mapper.component.ts | 10 ++++++---- .../item-collection-mapper.component.ts | 10 ++++++---- .../collection-select/collection-select.component.html | 2 ++ .../item-select/item-select.component.html | 2 ++ 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index ba3d1549c1..82e372ea56 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -198,9 +198,11 @@ "error.browse-by": "Error fetching items", "error.collection": "Error fetching collection", + "error.collections": "Error fetching collections", "error.community": "Error fetching community", "error.default": "Error", "error.item": "Error fetching item", + "error.items": "Error fetching items", "error.objects": "Error fetching objects", "error.recent-submissions": "Error fetching recent submissions", "error.search-results": "Error fetching search results", @@ -430,9 +432,11 @@ "loading.browse-by": "Loading items...", "loading.browse-by-page": "Loading page...", "loading.collection": "Loading collection...", + "loading.collections": "Loading collections...", "loading.community": "Loading community...", "loading.default": "Loading...", "loading.item": "Loading item...", + "loading.items": "Loading items...", "loading.mydspace-results": "Loading items...", "loading.objects": "Loading...", "loading.recent-submissions": "Loading recent submissions...", diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 417b044277..8f46a82077 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -8,7 +8,7 @@ import { Collection } from '../../core/shared/collection.model'; import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { map, switchMap, take, tap } from 'rxjs/operators'; +import { map, startWith, switchMap, take, tap } from 'rxjs/operators'; import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -128,10 +128,12 @@ export class CollectionItemMapperComponent implements OnInit { scope: undefined, dsoType: DSpaceObjectType.ITEM, sort: this.defaultSortOptions - })); + })).pipe( + toDSpaceObjectListRD(), + startWith(undefined) + ); } - }), - toDSpaceObjectListRD() + }) ); } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index b8073edb39..a7a090f691 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -11,7 +11,7 @@ import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } fr import { ActivatedRoute, Router } from '@angular/router'; import { SearchService } from '../../../+search-page/search-service/search.service'; import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; -import { map, switchMap, take } from 'rxjs/operators'; +import { map, startWith, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -112,9 +112,11 @@ export class ItemCollectionMapperComponent implements OnInit { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query), dsoType: DSpaceObjectType.COLLECTION - })); - }), - toDSpaceObjectListRD() + })).pipe( + toDSpaceObjectListRD(), + startWith(undefined) + ); + }) ) as Observable>>; } diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index 3e2ecdaebf..44307859ad 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -24,6 +24,8 @@
+ +
diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 5dbc88e55d..6691be3584 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -28,6 +28,8 @@
+ +
From 8652c4bce8cd04869b9efe72ccfc603ccd8ca1bb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 30 Sep 2019 16:37:44 +0200 Subject: [PATCH 76/78] 62589: Fixed test imports --- .../collection-item-mapper.component.spec.ts | 4 +++- .../item-collection-mapper.component.spec.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 8332d20cea..0bbfb30821 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -41,6 +41,8 @@ import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../core/cache/response.models'; import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { RouteService } from '../../core/services/route.service'; +import { ErrorComponent } from '../../shared/error/error.component'; +import { LoadingComponent } from '../../shared/loading/loading.component'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -120,7 +122,7 @@ describe('CollectionItemMapperComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], - declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective], + declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective, ErrorComponent, LoadingComponent], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: Router, useValue: routerStub }, diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 018ed3f2ac..ed9351d5d2 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -37,6 +37,8 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { SearchFormComponent } from '../../../shared/search-form/search-form.component'; import { Collection } from '../../../core/shared/collection.model'; +import { ErrorComponent } from '../../../shared/error/error.component'; +import { LoadingComponent } from '../../../shared/loading/loading.component'; describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; @@ -99,7 +101,7 @@ describe('ItemCollectionMapperComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], - declarations: [ItemCollectionMapperComponent, CollectionSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective], + declarations: [ItemCollectionMapperComponent, CollectionSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective, ErrorComponent, LoadingComponent], providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: Router, useValue: routerStub }, From 5b776b605ab34150cc675e07e502e1a19bb59339 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 8 Oct 2019 13:06:31 +0200 Subject: [PATCH 77/78] 62589: Review 03-10-2019 Changes and fixes --- resources/i18n/en.json5 | 44 +++++++++++-------- .../collection-item-mapper.component.html | 26 ++++++----- .../collection-item-mapper.component.ts | 28 +++++------- .../collection-page-routing.module.ts | 2 +- .../item-collection-mapper.component.html | 8 +++- .../item-collection-mapper.component.ts | 21 ++++----- .../search-service/search.service.ts | 13 ++---- .../core/data/collection-data.service.spec.ts | 10 ----- src/app/core/data/collection-data.service.ts | 11 +---- src/app/core/data/item-data.service.ts | 10 ----- src/app/core/data/request.models.ts | 2 + .../collection-select.component.html | 15 +++++-- .../collection-select.component.spec.ts | 4 +- .../item-select/item-select.component.html | 23 +++++++--- .../item-select/item-select.component.spec.ts | 2 +- .../object-select/object-select.service.ts | 8 +++- .../search-form/search-form.component.ts | 8 +++- 17 files changed, 118 insertions(+), 117 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 82e372ea56..0ad08652b5 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -128,8 +128,28 @@ "collection.delete.notification.fail": "Collection could not be deleted", "collection.delete.notification.success": "Successfully deleted collection", "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", + "collection.edit.delete": "Delete this collection", "collection.edit.head": "Edit Collection", + + "collection.edit.item-mapper.cancel": "Cancel", + "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", + "collection.edit.item-mapper.confirm": "Map selected items", + "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", + "collection.edit.item-mapper.no-search": "Please enter a query to search", + "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", + "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", + "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", + "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", + "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", + "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", + "collection.edit.item-mapper.remove": "Remove selected item mappings", + "collection.edit.item-mapper.tabs.browse": "Browse mapped items", + "collection.edit.item-mapper.tabs.map": "Map new items", + "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", "collection.form.errors.title.required": "Please enter a collection name", @@ -139,29 +159,13 @@ "collection.form.tableofcontents": "News (HTML)", "collection.form.title": "Name", - "collection.item-mapper.cancel": "Cancel", - "collection.item-mapper.collection": "Collection: \"{{name}}\"", - "collection.item-mapper.confirm": "Map selected items", - "collection.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - "collection.item-mapper.head": "Item Mapper - Map Items from Other Collections", - "collection.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - "collection.item-mapper.notifications.map.error.head": "Mapping errors", - "collection.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - "collection.item-mapper.notifications.map.success.head": "Mapping completed", - "collection.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - "collection.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - "collection.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - "collection.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - "collection.item-mapper.remove": "Remove selected item mappings", - "collection.item-mapper.tabs.browse": "Browse", - "collection.item-mapper.tabs.map": "Map", - "collection.page.browse.recent.head": "Recent Submissions", "collection.page.browse.recent.empty": "No items to show", "collection.page.license": "License", "collection.page.news": "News", "collection.select.confirm": "Confirm selected", + "collection.select.empty": "No collections to show", "collection.select.table.title": "Title", "community.create.head": "Create a Community", @@ -258,6 +262,7 @@ "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.item": "Item: \"{{name}}\"", + "item.edit.item-mapper.no-search": "Please enter a query to search", "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", @@ -266,8 +271,8 @@ "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - "item.edit.item-mapper.tabs.browse": "Browse", - "item.edit.item-mapper.tabs.map": "Map", + "item.edit.item-mapper.tabs.browse": "Browse mapped collections", + "item.edit.item-mapper.tabs.map": "Map new collections", "item.edit.metadata.add-button": "Add", "item.edit.metadata.discard-button": "Discard", @@ -401,6 +406,7 @@ "item.page.uri": "URI", "item.select.confirm": "Confirm selected", + "item.select.empty": "No items to show", "item.select.table.author": "Author", "item.select.table.collection": "Collection", "item.select.table.title": "Title", diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html index 23e23b5c25..af4153220f 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -1,20 +1,20 @@
-

{{'collection.item-mapper.head' | translate}}

-

-

{{'collection.item-mapper.description' | translate}}

+

{{'collection.edit.item-mapper.head' | translate}}

+

+

{{'collection.edit.item-mapper.description' | translate}}

- +
- +
@@ -30,21 +30,25 @@ [query]="(searchOptions$ | async)?.query" [scope]="(searchOptions$ | async)?.scope" [currentUrl]="'./'" - [inPlaceSearch]="true"> + [inPlaceSearch]="true" + (submitSearch)="performedSearch = true">
-
+
+ diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 8f46a82077..4781e63c95 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -84,6 +84,12 @@ export class CollectionItemMapperComponent implements OnInit { */ shouldUpdate$: BehaviorSubject; + /** + * Track whether at least one search has been performed or not + * As soon as at least one search has been performed, we display the search results + */ + performedSearch = false; + constructor(private route: ActivatedRoute, private router: Router, @Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService, @@ -128,7 +134,7 @@ export class CollectionItemMapperComponent implements OnInit { scope: undefined, dsoType: DSpaceObjectType.ITEM, sort: this.defaultSortOptions - })).pipe( + }), 1000).pipe( toDSpaceObjectListRD(), startWith(undefined) ); @@ -154,7 +160,6 @@ export class CollectionItemMapperComponent implements OnInit { ); this.showNotifications(responses$, remove); - this.clearRequestCache(); } /** @@ -170,8 +175,8 @@ export class CollectionItemMapperComponent implements OnInit { const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful); if (successful.length > 0) { const successMessages = observableCombineLatest( - this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.head`), - this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.success.content`, { amount: successful.length }) + this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.success.head`), + this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.success.content`, { amount: successful.length }) ); successMessages.subscribe(([head, content]) => { @@ -180,8 +185,8 @@ export class CollectionItemMapperComponent implements OnInit { } if (unsuccessful.length > 0) { const unsuccessMessages = observableCombineLatest( - this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.head`), - this.translateService.get(`collection.item-mapper.notifications.${messageInsertion}.error.content`, { amount: unsuccessful.length }) + this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.error.head`), + this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.error.content`, { amount: unsuccessful.length }) ); unsuccessMessages.subscribe(([head, content]) => { @@ -194,21 +199,12 @@ export class CollectionItemMapperComponent implements OnInit { }); } - /** - * Clear all previous requests from cache in preparation of refreshing all lists - */ - private clearRequestCache() { - this.collectionRD$.pipe(take(1)).subscribe((collectionRD: RemoteData) => { - this.collectionDataService.clearMappedItemsRequests(collectionRD.payload.id); - this.searchService.clearDiscoveryRequests(); - }); - } - /** * Clear url parameters on tab change (temporary fix until pagination is improved) * @param event */ tabChange(event) { + this.performedSearch = false; this.router.navigateByUrl(this.getCurrentUrl()); } diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index ae5d04714a..66c623657d 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -64,7 +64,7 @@ const COLLECTION_EDIT_PATH = ':id/edit'; } }, { - path: ':id/mapper', + path: ':id/edit/mapper', component: CollectionItemMapperComponent, pathMatch: 'full', resolve: { diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index d4433cd4a2..43bf7ecd02 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -28,12 +28,13 @@ + [inPlaceSearch]="true" + (submitSearch)="performedSearch = true">
-
+
+ diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index a7a090f691..4ee8498bb7 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -69,6 +69,12 @@ export class ItemCollectionMapperComponent implements OnInit { */ shouldUpdate$: BehaviorSubject; + /** + * Track whether at least one search has been performed or not + * As soon as at least one search has been performed, we display the search results + */ + performedSearch = false; + constructor(private route: ActivatedRoute, private router: Router, private searchConfigService: SearchConfigurationService, @@ -112,7 +118,7 @@ export class ItemCollectionMapperComponent implements OnInit { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query), dsoType: DSpaceObjectType.COLLECTION - })).pipe( + }), 1000).pipe( toDSpaceObjectListRD(), startWith(undefined) ); @@ -146,7 +152,6 @@ export class ItemCollectionMapperComponent implements OnInit { ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add'); - this.clearRequestCache(); } /** @@ -161,7 +166,6 @@ export class ItemCollectionMapperComponent implements OnInit { ); this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove'); - this.clearRequestCache(); } /** @@ -209,21 +213,12 @@ export class ItemCollectionMapperComponent implements OnInit { }); } - /** - * Clear all previous requests from cache in preparation of refreshing all lists - */ - private clearRequestCache() { - this.itemRD$.pipe(take(1)).subscribe((itemRD: RemoteData) => { - this.itemDataService.clearMappedCollectionsRequests(itemRD.payload.id); - this.searchService.clearDiscoveryRequests(); - }); - } - /** * Clear url parameters on tab change (temporary fix until pagination is improved) * @param event */ tabChange(event) { + this.performedSearch = false; this.router.navigateByUrl(this.getCurrentUrl()); } diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 8374813bc7..bedae84eaa 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -100,9 +100,10 @@ export class SearchService implements OnDestroy { /** * Method to retrieve a paginated list of search results from the server * @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search + * @param responseMsToLive The amount of milliseconds for the response to live in cache * @returns {Observable>>>} Emits a paginated list with all search results found */ - search(searchOptions?: PaginatedSearchOptions): Observable>>> { + search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable>>> { const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { if (hasValue(searchOptions)) { @@ -122,6 +123,7 @@ export class SearchService implements OnDestroy { }; return Object.assign(request, { + responseMsToLive: hasValue(responseMsToLive) ? responseMsToLive : request.responseMsToLive, getResponseParser: getResponseParserFn }); }), @@ -373,15 +375,6 @@ export class SearchService implements OnDestroy { return '/search'; } - /** - * Clear all request cache related to discovery objects - */ - clearDiscoveryRequests() { - this.halService.getEndpoint(this.searchLinkPath).pipe(take(1)).subscribe((href: string) => { - this.requestService.removeByHrefSubstring(href); - }); - } - /** * Unsubscribe from the subscription */ diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index b0b3889c9c..5cb7fed5e4 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -41,14 +41,4 @@ describe('CollectionDataService', () => { }); }); - describe('clearMappedItemsRequests', () => { - beforeEach(() => { - service.clearMappedItemsRequests('collection-id'); - }); - - it('should remote request cache', () => { - expect(requestService.removeByHrefSubstring).toHaveBeenCalled(); - }); - }); - }); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index cd59e7ac65..e49267d1f2 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -125,6 +125,7 @@ export class CollectionDataService extends ComColDataService { map((endpoint: string) => { const request = new GetRequest(requestUuid, endpoint); return Object.assign(request, { + responseMsToLive: 0, getResponseParser(): GenericConstructor { return DSOResponseParsingService; } @@ -136,14 +137,4 @@ export class CollectionDataService extends ComColDataService { return this.rdbService.buildList(href$); } - /** - * Clears all requests (from cache) connected to the mappedItems endpoint - * @param collectionId - */ - clearMappedItemsRequests(collectionId: string) { - this.getMappedItemsEndpoint(collectionId).pipe(take(1)).subscribe((href: string) => { - this.requestService.removeByHrefSubstring(href); - }); - } - } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cec60f3305..de05dad0c1 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -148,16 +148,6 @@ export class ItemDataService extends DataService { return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); } - /** - * Clears all requests (from cache) connected to the mappedCollections endpoint - * @param itemId - */ - public clearMappedCollectionsRequests(itemId: string) { - this.getMappedCollectionsEndpoint(itemId).pipe(take(1)).subscribe((href: string) => { - this.requestService.removeByHrefSubstring(href); - }); - } - /** * Get the endpoint for item withdrawal and reinstatement * @param itemId diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index b327306fcb..b0efc0ce71 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -190,6 +190,8 @@ export class BrowseItemsRequest extends GetRequest { * Request to fetch the mapped collections of an item */ export class MappedCollectionsRequest extends GetRequest { + public responseMsToLive = 0; + getResponseParser(): GenericConstructor { return MappedCollectionsReponseParsingService; } diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index 44307859ad..c8a0c4b879 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -7,7 +7,7 @@ [collectionSize]="collectionsRD?.payload?.totalElements" [hidePagerWhenSinglePage]="true" [hideGear]="true"> -
+
@@ -24,11 +24,18 @@
+ -
+
- - +
diff --git a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts index c9f79f6af5..af7c01a3c5 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.spec.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.spec.ts @@ -16,7 +16,7 @@ import { CollectionSelectComponent } from './collection-select.component'; import { Collection } from '../../../core/shared/collection.model'; import { of } from 'rxjs/internal/observable/of'; -describe('ItemSelectComponent', () => { +describe('CollectionSelectComponent', () => { let comp: CollectionSelectComponent; let fixture: ComponentFixture; let objectSelectService: ObjectSelectService; @@ -43,7 +43,7 @@ describe('ItemSelectComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ - { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockCollectionList[1].id]) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 6691be3584..da31dbac65 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -7,7 +7,7 @@ [collectionSize]="itemsRD?.payload?.totalElements" [hidePagerWhenSinglePage]="true" [hideGear]="true"> -
+ + -
+
- - +
diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index 33fa4dcd7e..059e44064e 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -65,7 +65,7 @@ describe('ItemSelectComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])], declarations: [], providers: [ - { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, + { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockItemList[1].id]) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/object-select/object-select.service.ts b/src/app/shared/object-select/object-select.service.ts index 03ddc0078c..8e30bca24d 100644 --- a/src/app/shared/object-select/object-select.service.ts +++ b/src/app/shared/object-select/object-select.service.ts @@ -51,7 +51,13 @@ export class ObjectSelectService { */ getAllSelected(key: string): Observable { return this.appStore.select(objectSelectionListStateSelector).pipe( - map((state: ObjectSelectionListState) => Object.keys(state[key]).filter((id) => state[key][id].checked)) + map((state: ObjectSelectionListState) => { + if (hasValue(state[key])) { + return Object.keys(state[key]).filter((id) => state[key][id].checked); + } else { + return []; + } + }) ); } diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 7414dd70e6..6b81b103ca 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../empty.util'; @@ -56,6 +56,11 @@ export class SearchFormComponent { */ @Input() brandColor = 'primary'; + /** + * Output the search data on submit + */ + @Output() submitSearch = new EventEmitter(); + constructor(private router: Router, private searchService: SearchService) { } @@ -65,6 +70,7 @@ export class SearchFormComponent { */ onSubmit(data: any) { this.updateSearch(data); + this.submitSearch.emit(data); } /** From f7bd30cf124debff21314a0c7ba6a7833c92c284 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 10 Oct 2019 13:39:59 +0200 Subject: [PATCH 78/78] 62589: Item-Mapper Requests responseMsToLive to 10s --- .../collection-item-mapper/collection-item-mapper.component.ts | 2 +- .../item-collection-mapper/item-collection-mapper.component.ts | 2 +- src/app/core/data/request.models.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 4781e63c95..750578cc35 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -134,7 +134,7 @@ export class CollectionItemMapperComponent implements OnInit { scope: undefined, dsoType: DSpaceObjectType.ITEM, sort: this.defaultSortOptions - }), 1000).pipe( + }), 10000).pipe( toDSpaceObjectListRD(), startWith(undefined) ); diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 4ee8498bb7..97b8164a6e 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -118,7 +118,7 @@ export class ItemCollectionMapperComponent implements OnInit { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query), dsoType: DSpaceObjectType.COLLECTION - }), 1000).pipe( + }), 10000).pipe( toDSpaceObjectListRD(), startWith(undefined) ); diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index b0efc0ce71..43ab9e58e7 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -190,7 +190,7 @@ export class BrowseItemsRequest extends GetRequest { * Request to fetch the mapped collections of an item */ export class MappedCollectionsRequest extends GetRequest { - public responseMsToLive = 0; + public responseMsToLive = 10000; getResponseParser(): GenericConstructor { return MappedCollectionsReponseParsingService;