From 6b986c8c9172549b8a90c84401213c76b33d1c74 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Sep 2018 09:44:01 +0200 Subject: [PATCH 001/110] 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 002/110] 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 003/110] 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 004/110] 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 005/110] 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 006/110] 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 007/110] 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 008/110] 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 009/110] 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 010/110] 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 011/110] 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 012/110] 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 013/110] 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 014/110] 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 015/110] 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 016/110] 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 017/110] 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 018/110] 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 019/110] 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 020/110] 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 021/110] 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 022/110] 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 023/110] 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 024/110] 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 025/110] 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 026/110] 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 027/110] 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 028/110] 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 029/110] 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 030/110] 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 031/110] 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 032/110] 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 033/110] 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 034/110] 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 035/110] 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 036/110] 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 037/110] 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 038/110] 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 039/110] 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 040/110] 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 041/110] 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 042/110] 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 043/110] 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 044/110] 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 045/110] 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 046/110] 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 047/110] 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 048/110] 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 049/110] 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 050/110] 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 051/110] 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 052/110] 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 053/110] 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 054/110] 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 055/110] 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 056/110] 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 057/110] 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 058/110] 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 059/110] 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 060/110] 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 061/110] 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 062/110] 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 063/110] 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 064/110] 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 065/110] 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 066/110] 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 067/110] 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 068/110] 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 069/110] 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 1008bd16e2e338daf40cdbb3fc5824a2299eeac7 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 21 Aug 2019 17:35:16 +0200 Subject: [PATCH 070/110] 64424: Angular impact of changes to relationship type changes --- .../simple/item-types/shared/item-relationships-utils.ts | 8 ++++---- .../models/items/normalized-relationship-type.model.ts | 4 ++-- .../shared/item-relationships/relationship-type.model.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts index 65ad743245..091bd4f406 100644 --- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts @@ -58,10 +58,10 @@ export const filterRelationsByTypeLabel = (label: string, thisId?: string) => return relatedItems$.pipe( map((arr) => relsCurrentPage.filter((rel: Relationship, idx: number) => hasValue(relTypesCurrentPage[idx]) && ( - (hasNoValue(thisId) && (relTypesCurrentPage[idx].leftLabel === label || - relTypesCurrentPage[idx].rightLabel === label)) || - (thisId === arr[idx][0].id && relTypesCurrentPage[idx].leftLabel === label) || - (thisId === arr[idx][1].id && relTypesCurrentPage[idx].rightLabel === label) + (hasNoValue(thisId) && (relTypesCurrentPage[idx].leftwardType === label || + relTypesCurrentPage[idx].rightwardType === label)) || + (thisId === arr[idx][0].id && relTypesCurrentPage[idx].leftwardType === label) || + (thisId === arr[idx][1].id && relTypesCurrentPage[idx].rightwardType === label) ) )) ); diff --git a/src/app/core/cache/models/items/normalized-relationship-type.model.ts b/src/app/core/cache/models/items/normalized-relationship-type.model.ts index 800b27cd7e..23c3333a9b 100644 --- a/src/app/core/cache/models/items/normalized-relationship-type.model.ts +++ b/src/app/core/cache/models/items/normalized-relationship-type.model.ts @@ -23,7 +23,7 @@ export class NormalizedRelationshipType extends NormalizedObject Date: Tue, 10 Sep 2019 16:29:15 +0200 Subject: [PATCH 071/110] 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 072/110] 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 073/110] 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 05d85c2d18be99d16fcd14410e46c21e2c1ae8c0 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Mon, 23 Sep 2019 14:32:57 -0700 Subject: [PATCH 074/110] create docker compose files in code base --- docker-compose-travis.yml | 53 +++++++++++++++++++ docker-compose.yml | 80 +++++++++++++++++++++++++++++ to_be_determined/environment.dev.js | 16 ++++++ to_be_determined/local.cfg | 6 +++ 4 files changed, 155 insertions(+) create mode 100644 docker-compose-travis.yml create mode 100644 docker-compose.yml create mode 100644 to_be_determined/environment.dev.js create mode 100644 to_be_determined/local.cfg diff --git a/docker-compose-travis.yml b/docker-compose-travis.yml new file mode 100644 index 0000000000..5a9ea9c3d6 --- /dev/null +++ b/docker-compose-travis.yml @@ -0,0 +1,53 @@ +networks: + dspacenet: {} +services: + dspace: + container_name: dspace + depends_on: + - dspacedb + image: dspace/dspace:dspace-7_x-jdk8-test + networks: + dspacenet: {} + ports: + - published: 8080 + target: 8080 + stdin_open: true + tty: true + volumes: + - assetstore:/dspace/assetstore + - ./to_be_determined:/dspace/config/local.cfg + dspacedb: + container_name: dspacedb + environment: + LOADSQL: https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1 + PGDATA: /pgdata + image: dspace/dspace-postgres-pgcrypto:loadsql + networks: + dspacenet: {} + stdin_open: true + tty: true + volumes: + - pgdata:/pgdata + dspacesolr: + container_name: dspacesolr + image: dspace/dspace-solr + networks: + dspacenet: {} + ports: + - published: 8983 + target: 8983 + stdin_open: true + tty: true + volumes: + - solr_authority:/opt/solr/server/solr/authority/data + - solr_oai:/opt/solr/server/solr/oai/data + - solr_search:/opt/solr/server/solr/search/data + - solr_statistics:/opt/solr/server/solr/statistics/data +version: '3.7' +volumes: + assetstore: {} + pgdata: {} + solr_authority: {} + solr_oai: {} + solr_search: {} + solr_statistics: {} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..b83b1b1e66 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,80 @@ +version: '3.7' +networks: + dspacenet: +services: + dspace: + container_name: dspace + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-jdk8-test}" + depends_on: + - dspacedb + networks: + dspacenet: + ports: + - published: 8080 + target: 8080 + stdin_open: true + tty: true + volumes: + - assetstore:/dspace/assetstore + - ./to_be_determined/local.cfg:/dspace/config/local.cfg + dspace-angular: + container_name: dspace-angular + depends_on: + - dspace + environment: + DSPACE_HOST: dspace-angular + DSPACE_NAMESPACE: / + DSPACE_PORT: '3000' + DSPACE_REST_HOST: dspace + DSPACE_REST_NAMESPACE: / + DSPACE_REST_PORT: '8080' + DSPACE_REST_SSL: "false" + DSPACE_SSL: "false" + image: dspace/dspace-angular:latest + build: + context: . + dockerfile: Dockerfile + networks: + dspacenet: {} + ports: + - published: 3000 + target: 3000 + - published: 9876 + target: 9876 + stdin_open: true + tty: true + volumes: + - ./to_be_determined/environment.dev.js:/app/config/environment.dev.js + dspacedb: + container_name: dspacedb + environment: + PGDATA: /pgdata + image: dspace/dspace-postgres-pgcrypto + networks: + dspacenet: + stdin_open: true + tty: true + volumes: + - pgdata:/pgdata + dspacesolr: + container_name: dspacesolr + image: dspace/dspace-solr + networks: + dspacenet: + ports: + - published: 8983 + target: 8983 + stdin_open: true + tty: true + volumes: + - solr_authority:/opt/solr/server/solr/authority/data + - solr_oai:/opt/solr/server/solr/oai/data + - solr_search:/opt/solr/server/solr/search/data + - solr_statistics:/opt/solr/server/solr/statistics/data +volumes: + assetstore: + pgdata: + solr_authority: + solr_oai: + solr_search: + solr_statistics: diff --git a/to_be_determined/environment.dev.js b/to_be_determined/environment.dev.js new file mode 100644 index 0000000000..f88506012f --- /dev/null +++ b/to_be_determined/environment.dev.js @@ -0,0 +1,16 @@ +/* + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +module.exports = { + rest: { + ssl: false, + host: 'localhost', + port: 8080, + // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript + nameSpace: '/server/api' + } +}; diff --git a/to_be_determined/local.cfg b/to_be_determined/local.cfg new file mode 100644 index 0000000000..6692b13658 --- /dev/null +++ b/to_be_determined/local.cfg @@ -0,0 +1,6 @@ +dspace.dir=/dspace +db.url=jdbc:postgresql://dspacedb:5432/dspace +dspace.hostname=dspace +dspace.baseUrl=http://localhost:8080 +dspace.name=DSpace Started with Docker Compose +solr.server=http://dspacesolr:8983/solr From 314c33b011d9fc881b036a0c474bfe65ffa15759 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Mon, 23 Sep 2019 16:56:07 -0700 Subject: [PATCH 075/110] reference compose files in travis.yml --- .travis.yml | 7 +++---- to_be_determined/cli.assetstore.yml | 20 ++++++++++++++++++++ to_be_determined/cli.yml | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 to_be_determined/cli.assetstore.yml create mode 100644 to_be_determined/cli.yml diff --git a/.travis.yml b/.travis.yml index 3abd52c25f..8a86848f1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,13 +17,12 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin - - git clone https://github.com/DSpace-Labs/DSpace-Docker-Images.git install: # Start up DSpace 7 using the entities database dump - - docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.travis.ci.yml up -d + - docker-compose -f docker-compose-travis.yml up -d # Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update - - docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.cli.yml -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.cli.assetstore.yml run --rm dspace-cli + - docker-compose -f to_be_determined/cli.yml -f to_be_determined/cli.assetstore.yml run --rm dspace-cli - travis_retry yarn install before_script: @@ -32,7 +31,7 @@ before_script: #- curl http://localhost:8080/ after_script: - - docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.travis.ci.yml down + - docker-compose -f docker-compose-travis.yml down addons: apt: diff --git a/to_be_determined/cli.assetstore.yml b/to_be_determined/cli.assetstore.yml new file mode 100644 index 0000000000..0c6fcb2030 --- /dev/null +++ b/to_be_determined/cli.assetstore.yml @@ -0,0 +1,20 @@ +version: "3.7" + +services: + dspace-cli: + environment: + - LOADASSETS=https://www.dropbox.com/s/zv7lj8j2lp3egjs/assetstore.tar.gz?dl=1 + entrypoint: + - /bin/bash + - '-c' + - | + if [ ! -z $${LOADASSETS} ] + then + curl $${LOADASSETS} -L -s --output /tmp/assetstore.tar.gz + cd /dspace + tar xvfz /tmp/assetstore.tar.gz + fi + + /dspace/bin/dspace index-discovery + /dspace/bin/dspace oai import + /dspace/bin/dspace oai clean-cache diff --git a/to_be_determined/cli.yml b/to_be_determined/cli.yml new file mode 100644 index 0000000000..ea5e3e0595 --- /dev/null +++ b/to_be_determined/cli.yml @@ -0,0 +1,22 @@ +version: "3.7" + +services: + dspace-cli: + image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" + container_name: dspace-cli + #environment: + volumes: + - "assetstore:/dspace/assetstore" + - "./local.cfg:/dspace/config/local.cfg" + entrypoint: /dspace/bin/dspace + command: help + networks: + - dspacenet + tty: true + stdin_open: true + +volumes: + assetstore: + +networks: + dspacenet: From dfcb5abd493b8fb7115728b0701cf5c9e29050b7 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Wed, 25 Sep 2019 14:56:57 -0700 Subject: [PATCH 076/110] change path to component compose files --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8a86848f1b..12627a632e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: # Start up DSpace 7 using the entities database dump - docker-compose -f docker-compose-travis.yml up -d # Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update - - docker-compose -f to_be_determined/cli.yml -f to_be_determined/cli.assetstore.yml run --rm dspace-cli + - docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli - travis_retry yarn install before_script: From 71287b6b41f32b0f27b9467583dca7a610b43c95 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Wed, 25 Sep 2019 15:03:46 -0700 Subject: [PATCH 077/110] travis troubleshooting --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 12627a632e..2a47199dd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: # Start up DSpace 7 using the entities database dump - docker-compose -f docker-compose-travis.yml up -d # Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update - - docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli + #- docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli - travis_retry yarn install before_script: From 5dfc2fffcaec60d6d1a48713c4a905bc75892510 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Wed, 25 Sep 2019 15:09:36 -0700 Subject: [PATCH 078/110] fix compose file --- .travis.yml | 2 +- docker-compose-travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2a47199dd1..12627a632e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ install: # Start up DSpace 7 using the entities database dump - docker-compose -f docker-compose-travis.yml up -d # Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update - #- docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli + - docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli - travis_retry yarn install before_script: diff --git a/docker-compose-travis.yml b/docker-compose-travis.yml index 5a9ea9c3d6..de1278d90d 100644 --- a/docker-compose-travis.yml +++ b/docker-compose-travis.yml @@ -15,7 +15,7 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - ./to_be_determined:/dspace/config/local.cfg + - ./to_be_determined/local.cfg:/dspace/config/local.cfg dspacedb: container_name: dspacedb environment: From e68cbcd28c5181538ee0ae11057af756d51704fd Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 27 Sep 2019 14:57:07 +0200 Subject: [PATCH 079/110] add header icon color variable and add !default to all themeable variables --- src/app/header/header.component.scss | 11 ++++++ src/styles/_bootstrap_variables.scss | 18 +++++----- src/styles/_custom_variables.scss | 53 ++++++++++++++-------------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss index 4d25bd0d43..70c66f119d 100644 --- a/src/app/header/header.component.scss +++ b/src/app/header/header.component.scss @@ -8,3 +8,14 @@ background-image: none !important; line-height: 1.5; } + +.navbar ::ng-deep { + a { + color: $header-icon-color; + + &:hover, &focus { + color: darken($header-icon-color, 15%); + } + } +} + diff --git a/src/styles/_bootstrap_variables.scss b/src/styles/_bootstrap_variables.scss index 5258365cfd..6af8f1d170 100644 --- a/src/styles/_bootstrap_variables.scss +++ b/src/styles/_bootstrap_variables.scss @@ -1,16 +1,16 @@ @import '_themed_bootstrap_variables.scss'; /** Help Variables **/ -$fa-fixed-width: 1.25rem; -$icon-padding: 1rem; -$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)); -$sidebar-items-width: 250px; -$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width; +$fa-fixed-width: 1.25rem !default; +$icon-padding: 1rem !default; +$collapsed-sidebar-width: calculatePx($fa-fixed-width + (2 * $icon-padding)) !default; +$sidebar-items-width: 250px !default; +$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default; /* Fonts */ -$fa-font-path: "../assets/fonts"; +$fa-font-path: "../assets/fonts" !default; /* Images */ -$image-path: "../assets/images"; +$image-path: "../assets/images" !default; /** Bootstrap Variables **/ /* Colors */ @@ -44,8 +44,8 @@ $link-color: map-get($theme-colors, info) !default; $navbar-dark-color: rgba(white, .5) !default; $navbar-light-color: rgba(black, .5) !default; -$navbar-dark-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-dark-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E"); -$navbar-light-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-light-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E"); +$navbar-dark-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-dark-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E") !default; +$navbar-light-toggler-icon-bg: url("data%3Aimage%2Fsvg+xml%3Bcharset%3Dutf8%2C%3Csvg+viewBox%3D%270+0+30+30%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath+stroke%3D%27#{$navbar-light-color}%27+stroke-width%3D%272%27+stroke-linecap%3D%27round%27+stroke-miterlimit%3D%2710%27+d%3D%27M4+7h22M4+15h22M4+23h22%27%2F%3E%3C%2Fsvg%3E") !default; $enable-shadows: true !default; diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index df193be91b..c1f155fa39 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -1,38 +1,39 @@ @import '_themed_custom_variables.scss'; -$content-spacing: $spacer * 1.5; +$content-spacing: $spacer * 1.5 !default; -$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2); +$button-height: $input-btn-padding-y * 2 + $input-btn-line-height + calculateRem($input-btn-border-width*2) !default; -$card-height-percentage:98%; -$card-thumbnail-height:240px; -$dropdown-menu-max-height: 200px; -$drop-zone-area-height: 44px; -$drop-zone-area-z-index: 1025; -$drop-zone-area-inner-z-index: 1021; -$login-logo-height:72px; -$login-logo-width:72px; -$submission-header-z-index: 1001; -$submission-footer-z-index: 999; +$card-height-percentage:98% !default; +$card-thumbnail-height:240px !default; +$dropdown-menu-max-height: 200px !default; +$drop-zone-area-height: 44px !default; +$drop-zone-area-z-index: 1025 !default; +$drop-zone-area-inner-z-index: 1021 !default; +$login-logo-height:72px !default; +$login-logo-width:72px !default; +$submission-header-z-index: 1001 !default; +$submission-footer-z-index: 999 !default; -$main-z-index: 0; -$nav-z-index: 10; -$sidebar-z-index: 20; +$main-z-index: 0 !default; +$nav-z-index: 10 !default; +$sidebar-z-index: 20 !default; -$header-logo-height: 80px; -$header-logo-height-xs: 50px; +$header-logo-height: 80px !default; +$header-logo-height-xs: 50px !default; +$header-icon-color: $link-color !default; -$admin-sidebar-bg: darken(#2B4E72, 17%); -$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%); -$admin-sidebar-header-bg: darken($admin-sidebar-bg, 7%); +$admin-sidebar-bg: darken(#2B4E72, 17%) !default; +$admin-sidebar-active-bg: darken($admin-sidebar-bg, 3%) !default; +$admin-sidebar-header-bg: darken($admin-sidebar-bg, 7%) !default; -$dark-scrollbar-background: $admin-sidebar-active-bg; -$dark-scrollbar-foreground: #47495d; +$dark-scrollbar-background: $admin-sidebar-active-bg !default; +$dark-scrollbar-foreground: #47495d !default; $submission-sections-margin-bottom: .5rem !default; -$edit-item-button-min-width: 100px; -$edit-item-metadata-field-width: 190px; -$edit-item-language-field-width: 43px; +$edit-item-button-min-width: 100px !default; +$edit-item-metadata-field-width: 190px !default; +$edit-item-language-field-width: 43px !default; -$thumbnail-max-width: 175px; +$thumbnail-max-width: 175px !default; From 145ed346c85c4ae13b26e834f9c17febc5c6cea7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 27 Sep 2019 16:40:47 +0200 Subject: [PATCH 080/110] 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 c060099ff8636ad6ab95d262eb2c18e3cd644f9e Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Sun, 29 Sep 2019 10:44:42 -0700 Subject: [PATCH 081/110] run angular on its own --- docker-compose.yml | 47 ---------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b83b1b1e66..3573076724 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,21 +2,6 @@ version: '3.7' networks: dspacenet: services: - dspace: - container_name: dspace - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-jdk8-test}" - depends_on: - - dspacedb - networks: - dspacenet: - ports: - - published: 8080 - target: 8080 - stdin_open: true - tty: true - volumes: - - assetstore:/dspace/assetstore - - ./to_be_determined/local.cfg:/dspace/config/local.cfg dspace-angular: container_name: dspace-angular depends_on: @@ -45,36 +30,4 @@ services: tty: true volumes: - ./to_be_determined/environment.dev.js:/app/config/environment.dev.js - dspacedb: - container_name: dspacedb - environment: - PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto - networks: - dspacenet: - stdin_open: true - tty: true - volumes: - - pgdata:/pgdata - dspacesolr: - container_name: dspacesolr - image: dspace/dspace-solr - networks: - dspacenet: - ports: - - published: 8983 - target: 8983 - stdin_open: true - tty: true - volumes: - - solr_authority:/opt/solr/server/solr/authority/data - - solr_oai:/opt/solr/server/solr/oai/data - - solr_search:/opt/solr/server/solr/search/data - - solr_statistics:/opt/solr/server/solr/statistics/data volumes: - assetstore: - pgdata: - solr_authority: - solr_oai: - solr_search: - solr_statistics: From 824289cde13744f0cdb9ee4d39b0e2dee5e5fee9 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Sun, 29 Sep 2019 11:19:56 -0700 Subject: [PATCH 082/110] fix compose file --- docker-compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3573076724..5af000ee68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ networks: services: dspace-angular: container_name: dspace-angular - depends_on: - - dspace environment: DSPACE_HOST: dspace-angular DSPACE_NAMESPACE: / @@ -30,4 +28,3 @@ services: tty: true volumes: - ./to_be_determined/environment.dev.js:/app/config/environment.dev.js -volumes: From eca35851d3bd128aec16bbee81146542865b6c50 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Sun, 29 Sep 2019 11:58:50 -0700 Subject: [PATCH 083/110] match network name --- to_be_determined/cli.assetstore.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/to_be_determined/cli.assetstore.yml b/to_be_determined/cli.assetstore.yml index 0c6fcb2030..ad1fdd8455 100644 --- a/to_be_determined/cli.assetstore.yml +++ b/to_be_determined/cli.assetstore.yml @@ -1,7 +1,12 @@ version: "3.7" +networks: + dspacenet: + services: dspace-cli: + networks: + dspacenet: {} environment: - LOADASSETS=https://www.dropbox.com/s/zv7lj8j2lp3egjs/assetstore.tar.gz?dl=1 entrypoint: From a2fb8a316b3c47b85a995db892839c54907738c0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 30 Sep 2019 10:35:52 +0200 Subject: [PATCH 084/110] 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 085/110] 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 086/110] 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 c6d2cb66c750c1533041c561ad09a3e1cf759215 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Mon, 30 Sep 2019 10:45:45 -0700 Subject: [PATCH 087/110] refactor docker compse names --- .travis.yml | 6 +-- docker-compose.yml | 2 +- .../cli.assetstore.yml | 0 {to_be_determined => docker}/cli.yml | 0 docker/docker-compose-rest.yml | 50 +++++++++++++++++++ .../docker-compose-travis.yml | 2 +- .../environment.dev.js | 0 {to_be_determined => docker}/local.cfg | 0 8 files changed, 55 insertions(+), 5 deletions(-) rename {to_be_determined => docker}/cli.assetstore.yml (100%) rename {to_be_determined => docker}/cli.yml (100%) create mode 100644 docker/docker-compose-rest.yml rename docker-compose-travis.yml => docker/docker-compose-travis.yml (95%) rename {to_be_determined => docker}/environment.dev.js (100%) rename {to_be_determined => docker}/local.cfg (100%) diff --git a/.travis.yml b/.travis.yml index 12627a632e..901dee8186 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,9 +20,9 @@ before_install: install: # Start up DSpace 7 using the entities database dump - - docker-compose -f docker-compose-travis.yml up -d + - docker-compose -f ./docker/docker-compose-travis.yml up -d # Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update - - docker-compose -f ./to_be_determined/cli.yml -f ./to_be_determined/cli.assetstore.yml run --rm dspace-cli + - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli - travis_retry yarn install before_script: @@ -31,7 +31,7 @@ before_script: #- curl http://localhost:8080/ after_script: - - docker-compose -f docker-compose-travis.yml down + - docker-compose -f ./docker/docker-compose-travis.yml down addons: apt: diff --git a/docker-compose.yml b/docker-compose.yml index 5af000ee68..95cc98c4ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,4 +27,4 @@ services: stdin_open: true tty: true volumes: - - ./to_be_determined/environment.dev.js:/app/config/environment.dev.js + - ./docker/environment.dev.js:/app/config/environment.dev.js diff --git a/to_be_determined/cli.assetstore.yml b/docker/cli.assetstore.yml similarity index 100% rename from to_be_determined/cli.assetstore.yml rename to docker/cli.assetstore.yml diff --git a/to_be_determined/cli.yml b/docker/cli.yml similarity index 100% rename from to_be_determined/cli.yml rename to docker/cli.yml diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml new file mode 100644 index 0000000000..051a82a382 --- /dev/null +++ b/docker/docker-compose-rest.yml @@ -0,0 +1,50 @@ +networks: + dspacenet: {} +services: + dspace: + container_name: dspace + depends_on: + - dspacedb + image: dspace/dspace:dspace-7_x-jdk8-test + networks: + dspacenet: {} + ports: + - published: 8080 + target: 8080 + stdin_open: true + tty: true + volumes: + - assetstore:/dspace/assetstore + - ./local.cfg:/dspace/config/local.cfg + dspacedb: + container_name: dspacedb + image: dspace/dspace-postgres-pgcrypto + networks: + dspacenet: {} + stdin_open: true + tty: true + volumes: + - pgdata:/pgdata + dspacesolr: + container_name: dspacesolr + image: dspace/dspace-solr + networks: + dspacenet: {} + ports: + - published: 8983 + target: 8983 + stdin_open: true + tty: true + volumes: + - solr_authority:/opt/solr/server/solr/authority/data + - solr_oai:/opt/solr/server/solr/oai/data + - solr_search:/opt/solr/server/solr/search/data + - solr_statistics:/opt/solr/server/solr/statistics/data +version: '3.7' +volumes: + assetstore: {} + pgdata: {} + solr_authority: {} + solr_oai: {} + solr_search: {} + solr_statistics: {} diff --git a/docker-compose-travis.yml b/docker/docker-compose-travis.yml similarity index 95% rename from docker-compose-travis.yml rename to docker/docker-compose-travis.yml index de1278d90d..652043c7ae 100644 --- a/docker-compose-travis.yml +++ b/docker/docker-compose-travis.yml @@ -15,7 +15,7 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - ./to_be_determined/local.cfg:/dspace/config/local.cfg + - ./local.cfg:/dspace/config/local.cfg dspacedb: container_name: dspacedb environment: diff --git a/to_be_determined/environment.dev.js b/docker/environment.dev.js similarity index 100% rename from to_be_determined/environment.dev.js rename to docker/environment.dev.js diff --git a/to_be_determined/local.cfg b/docker/local.cfg similarity index 100% rename from to_be_determined/local.cfg rename to docker/local.cfg From f85d4fa033e8892aca9a407dd185a2b30d956056 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 10:31:07 -0700 Subject: [PATCH 088/110] Add README for Docker --- README.md | 5 +++++ docker/README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 docker/README.md diff --git a/README.md b/README.md index 1b3ed9b7cb..a9f2b0861b 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,11 @@ yarn run clean:prod yarn run clean:dist ``` +Running the application with Docker +----------------------------------- +See [Docker Runtime Options](docker/README.md) + + Testing ------- diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000000..645784e0fb --- /dev/null +++ b/docker/README.md @@ -0,0 +1,52 @@ +# Docker Compose files + +## root directory +- docker-compose.yml + - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. + +## docker directory +- docker-compose-rest.yml + - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes +- docker-compose-travis.yml + - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup. +- cli.yml + - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. +- cli.assetstore.yml + - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing. +- environment.dev.js + - Environment file for running DSpace Angular in Docker +- local.cfg + - Environment file for running the DSpace 7 REST API in Docker. + + + ## To start DSpace from your branch using a published images for DSpace REST and DSpace Angular. + ``` + docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up -d + ``` + + ## To build DSpace Angular from your branch using a published image for DSpace REST. + ``` + docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up --build -d + ``` + + ## To build DSpace REST and DSpace Angular. + _The system will be started in 2 steps. Each step shares the same docker network._ + + From DSpace/DSpace + ``` + docker-compose -p d7 up --build -d + ``` + + + From DSpace/DSpace-angular + ``` + docker-compose -p d7 up --build -d + ``` + + ## End to end testing of the rest api (runs in travis). + _In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ + + + ``` + docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d + ``` From 292f7ddd25ad8da4efc7f6e7e7fea7f2b2283bea Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 15:38:19 -0700 Subject: [PATCH 089/110] Add pull vs build instructions to README --- docker/README.md | 51 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docker/README.md b/docker/README.md index 645784e0fb..1c490ae02c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -19,34 +19,37 @@ - Environment file for running the DSpace 7 REST API in Docker. - ## To start DSpace from your branch using a published images for DSpace REST and DSpace Angular. - ``` - docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up -d - ``` +## To refresh / pull DSpace images from Dockerhub +``` +docker-compose pull +``` - ## To build DSpace Angular from your branch using a published image for DSpace REST. - ``` - docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up --build -d - ``` +## To build DSpace images using code in your branch +``` +docker-compose build +``` - ## To build DSpace REST and DSpace Angular. - _The system will be started in 2 steps. Each step shares the same docker network._ +## To start DSpace (REST and Angular) from your branch +``` +docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up -d +``` - From DSpace/DSpace - ``` - docker-compose -p d7 up --build -d - ``` +## To build DSpace REST and DSpace Angular. +_The system will be started in 2 steps. Each step shares the same docker network._ +From DSpace/DSpace +``` +docker-compose -p d7 up --build -d +``` - From DSpace/DSpace-angular - ``` - docker-compose -p d7 up --build -d - ``` +From DSpace/DSpace-angular +``` +docker-compose -p d7 up --build -d +``` - ## End to end testing of the rest api (runs in travis). - _In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ +## End to end testing of the rest api (runs in travis). +_In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ - - ``` - docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d - ``` +``` +docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d +``` From a74f256ceabc67c470c52baf5be9fbf3206d7bd3 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 22:52:44 -0700 Subject: [PATCH 090/110] refine startup instructions --- docker-compose.yml | 6 +----- docker/README.md | 24 +++++++++++++++++++----- docker/cli.assetstore.yml | 2 -- docker/cli.ingest.yml | 32 ++++++++++++++++++++++++++++++++ docker/db.entities.yml | 16 ++++++++++++++++ docker/docker-compose-rest.yml | 29 ++++++++++++++++++----------- docker/docker-compose-travis.yml | 22 +++++++++++----------- 7 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 docker/cli.ingest.yml create mode 100644 docker/db.entities.yml diff --git a/docker-compose.yml b/docker-compose.yml index 95cc98c4ae..e6b4eb3d6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,17 +8,13 @@ services: DSPACE_HOST: dspace-angular DSPACE_NAMESPACE: / DSPACE_PORT: '3000' - DSPACE_REST_HOST: dspace - DSPACE_REST_NAMESPACE: / - DSPACE_REST_PORT: '8080' - DSPACE_REST_SSL: "false" DSPACE_SSL: "false" image: dspace/dspace-angular:latest build: context: . dockerfile: Dockerfile networks: - dspacenet: {} + dspacenet: ports: - published: 3000 target: 3000 diff --git a/docker/README.md b/docker/README.md index 1c490ae02c..6a5077303b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -30,23 +30,37 @@ docker-compose build ``` ## To start DSpace (REST and Angular) from your branch + ``` -docker-compose -p d7 -f docker-compose.yml -f docker/docker-compose-rest.yml up -d +docker-compose -p d7 -f docker/docker-compose-rest.yml -f docker-compose.yml up -d ``` -## To build DSpace REST and DSpace Angular. +## Run DSpace REST and DSpace Angular from local branches. _The system will be started in 2 steps. Each step shares the same docker network._ -From DSpace/DSpace +From DSpace/DSpace (build as needed) ``` -docker-compose -p d7 up --build -d +docker-compose -p d7 up -d ``` From DSpace/DSpace-angular ``` -docker-compose -p d7 up --build -d +docker-compose -p d7 up -d ``` +## Ingest test data from AIPDIR + +Create an administrator +``` +docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en +``` + +Load content from AIP files +``` +docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli +``` + + ## End to end testing of the rest api (runs in travis). _In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index ad1fdd8455..075c494a6c 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -21,5 +21,3 @@ services: fi /dspace/bin/dspace index-discovery - /dspace/bin/dspace oai import - /dspace/bin/dspace oai clean-cache diff --git a/docker/cli.ingest.yml b/docker/cli.ingest.yml new file mode 100644 index 0000000000..f5ec7eb90d --- /dev/null +++ b/docker/cli.ingest.yml @@ -0,0 +1,32 @@ +# +# The contents of this file are subject to the license and copyright +# detailed in the LICENSE and NOTICE files at the root of the source +# tree and available online at +# +# http://www.dspace.org/license/ +# + +version: "3.7" + +services: + dspace-cli: + environment: + - AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/master/dogAndReport.zip + - ADMIN_EMAIL=test@test.edu + - AIPDIR=/tmp/aip-dir + entrypoint: + - /bin/bash + - '-c' + - | + rm -rf $${AIPDIR} + mkdir $${AIPDIR} /dspace/upload + cd $${AIPDIR} + pwd + curl $${AIPZIP} -L -s --output aip.zip + unzip aip.zip + cd $${AIPDIR} + + /dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip + /dspace/bin/dspace database update-sequences + + /dspace/bin/dspace index-discovery diff --git a/docker/db.entities.yml b/docker/db.entities.yml new file mode 100644 index 0000000000..91d96bd72b --- /dev/null +++ b/docker/db.entities.yml @@ -0,0 +1,16 @@ +# +# The contents of this file are subject to the license and copyright +# detailed in the LICENSE and NOTICE files at the root of the source +# tree and available online at +# +# http://www.dspace.org/license/ +# + +version: "3.7" + +services: + dspacedb: + image: dspace/dspace-postgres-pgcrypto:loadsql + environment: + # Double underbars in env names will be replaced with periods for apache commons + - LOADSQL=https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1 diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index 051a82a382..b861cdcfec 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -1,5 +1,5 @@ networks: - dspacenet: {} + dspacenet: services: dspace: container_name: dspace @@ -7,7 +7,7 @@ services: - dspacedb image: dspace/dspace:dspace-7_x-jdk8-test networks: - dspacenet: {} + dspacenet: ports: - published: 8080 target: 8080 @@ -15,12 +15,19 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - ./local.cfg:/dspace/config/local.cfg + - "./local.cfg:/dspace/config/local.cfg" + # Ensure that the database is ready before starting tomcat + entrypoint: + - /bin/bash + - '-c' + - | + /dspace/bin/dspace database migrate + catalina.sh run dspacedb: container_name: dspacedb image: dspace/dspace-postgres-pgcrypto networks: - dspacenet: {} + dspacenet: stdin_open: true tty: true volumes: @@ -29,7 +36,7 @@ services: container_name: dspacesolr image: dspace/dspace-solr networks: - dspacenet: {} + dspacenet: ports: - published: 8983 target: 8983 @@ -42,9 +49,9 @@ services: - solr_statistics:/opt/solr/server/solr/statistics/data version: '3.7' volumes: - assetstore: {} - pgdata: {} - solr_authority: {} - solr_oai: {} - solr_search: {} - solr_statistics: {} + assetstore: + pgdata: + solr_authority: + solr_oai: + solr_search: + solr_statistics: diff --git a/docker/docker-compose-travis.yml b/docker/docker-compose-travis.yml index 652043c7ae..6ca44e4e47 100644 --- a/docker/docker-compose-travis.yml +++ b/docker/docker-compose-travis.yml @@ -1,5 +1,5 @@ networks: - dspacenet: {} + dspacenet: services: dspace: container_name: dspace @@ -7,7 +7,7 @@ services: - dspacedb image: dspace/dspace:dspace-7_x-jdk8-test networks: - dspacenet: {} + dspacenet: ports: - published: 8080 target: 8080 @@ -15,7 +15,7 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - - ./local.cfg:/dspace/config/local.cfg + - "./local.cfg:/dspace/config/local.cfg" dspacedb: container_name: dspacedb environment: @@ -23,7 +23,7 @@ services: PGDATA: /pgdata image: dspace/dspace-postgres-pgcrypto:loadsql networks: - dspacenet: {} + dspacenet: stdin_open: true tty: true volumes: @@ -32,7 +32,7 @@ services: container_name: dspacesolr image: dspace/dspace-solr networks: - dspacenet: {} + dspacenet: ports: - published: 8983 target: 8983 @@ -45,9 +45,9 @@ services: - solr_statistics:/opt/solr/server/solr/statistics/data version: '3.7' volumes: - assetstore: {} - pgdata: {} - solr_authority: {} - solr_oai: {} - solr_search: {} - solr_statistics: {} + assetstore: + pgdata: + solr_authority: + solr_oai: + solr_search: + solr_statistics: From dc47d191ecb200cc9a9fb972090eb294206a48b7 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 23:30:26 -0700 Subject: [PATCH 091/110] move all compose files to one dir --- docker/README.md | 16 ++++++++++------ docker/docker-compose-rest.yml | 2 ++ docker-compose.yml => docker/docker-compose.yml | 6 +++--- 3 files changed, 15 insertions(+), 9 deletions(-) rename docker-compose.yml => docker/docker-compose.yml (82%) diff --git a/docker/README.md b/docker/README.md index 6a5077303b..097ab37694 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,10 +1,8 @@ # Docker Compose files -## root directory +## docker directory - docker-compose.yml - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - -## docker directory - docker-compose-rest.yml - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes - docker-compose-travis.yml @@ -21,18 +19,18 @@ ## To refresh / pull DSpace images from Dockerhub ``` -docker-compose pull +docker-compose -f docker/docker-compose.yml pull ``` ## To build DSpace images using code in your branch ``` -docker-compose build +docker-compose-f docker/docker-compose.yml build ``` ## To start DSpace (REST and Angular) from your branch ``` -docker-compose -p d7 -f docker/docker-compose-rest.yml -f docker-compose.yml up -d +docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` ## Run DSpace REST and DSpace Angular from local branches. @@ -60,6 +58,12 @@ Load content from AIP files docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli ``` +## Alternative Ingest - Use Entities dataset +_Delete your docker volumes or use a unique project (-p) name_ +``` +docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d +``` + ## End to end testing of the rest api (runs in travis). _In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index b861cdcfec..222557bc81 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -26,6 +26,8 @@ services: dspacedb: container_name: dspacedb image: dspace/dspace-postgres-pgcrypto + environment: + PGDATA: /pgdata networks: dspacenet: stdin_open: true diff --git a/docker-compose.yml b/docker/docker-compose.yml similarity index 82% rename from docker-compose.yml rename to docker/docker-compose.yml index e6b4eb3d6c..23f0615a1f 100644 --- a/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,10 +11,10 @@ services: DSPACE_SSL: "false" image: dspace/dspace-angular:latest build: - context: . + context: .. dockerfile: Dockerfile networks: - dspacenet: + dspacenet: ports: - published: 3000 target: 3000 @@ -23,4 +23,4 @@ services: stdin_open: true tty: true volumes: - - ./docker/environment.dev.js:/app/config/environment.dev.js + - ./environment.dev.js:/app/config/environment.dev.js From f8961d764749ae27a6df33e33e4b64bf1b54e2c5 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 23:32:45 -0700 Subject: [PATCH 092/110] readme update --- docker/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/README.md b/docker/README.md index 097ab37694..d1f2c95eeb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -60,10 +60,16 @@ docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspac ## Alternative Ingest - Use Entities dataset _Delete your docker volumes or use a unique project (-p) name_ + +Start DSpace with Database Content from a database dump ``` docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d ``` +Load assetstore content and trigger a re-index of the repository +``` +docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli +``` ## End to end testing of the rest api (runs in travis). _In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ From e4bab45f3cbc144dea50ea184979f7d7e465ff23 Mon Sep 17 00:00:00 2001 From: Terry Brady Date: Tue, 1 Oct 2019 23:47:01 -0700 Subject: [PATCH 093/110] correct path in README --- docker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index d1f2c95eeb..17a0dae9c4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -43,7 +43,7 @@ docker-compose -p d7 up -d From DSpace/DSpace-angular ``` -docker-compose -p d7 up -d +docker-compose -p d7 -f docker/docker-compose.yml up -d ``` ## Ingest test data from AIPDIR From 36d3ddd6919d8bd3fe8ce0fb84c56b4f3ad53633 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 13:55:57 +0200 Subject: [PATCH 094/110] leftLabel/rightLabel renaming to leftwardType/rightwardType --- src/app/core/data/relationship.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 1699b6a27d..b07e4b714c 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -182,9 +182,9 @@ export class RelationshipService { map(([leftItems, rightItems, relTypesCurrentPage]) => { return relTypesCurrentPage.map((type, index) => { if (leftItems[index].uuid === item.uuid) { - return type.leftLabel; + return type.leftwardType; } else { - return type.rightLabel; + return type.rightwardType; } }); }), From a1fea1d6a2ecc7a79e800964da437ead9e6e4902 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 14:53:00 +0200 Subject: [PATCH 095/110] leftLabel/rightLabel renaming to leftwardType/rightwardType in test case --- src/app/core/data/relationship.service.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 0ced517d74..75e0a856fa 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -5,7 +5,6 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-da import { of as observableOf } from 'rxjs/internal/observable/of'; import { RequestEntry } from './request.reducer'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { ResourceType } from '../shared/resource-type'; import { Relationship } from '../shared/item-relationships/relationship.model'; import { RemoteData } from './remote-data'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; @@ -33,8 +32,8 @@ describe('RelationshipService', () => { const relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftLabel: 'isAuthorOfPublication', - rightLabel: 'isPublicationOfAuthor' + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor' }); const relationship1 = Object.assign(new Relationship(), { @@ -129,7 +128,7 @@ describe('RelationshipService', () => { describe('getItemRelationshipLabels', () => { it('should return the correct labels', () => { service.getItemRelationshipLabels(item).subscribe((result) => { - expect(result).toEqual([relationshipType.rightLabel]); + expect(result).toEqual([relationshipType.rightwardType]); }); }); }); From 556475318dac77ec28043846527a9dd14f296b4e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 15:22:45 +0200 Subject: [PATCH 096/110] Fixed remaining left/right-labels --- .../edit-relationship-list.component.spec.ts | 6 +++--- .../edit-relationship/edit-relationship.component.spec.ts | 4 ++-- .../item-relationships/item-relationships.component.spec.ts | 4 ++-- src/app/core/data/relationship.service.spec.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 3748ebca9d..3293711d73 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -38,8 +38,8 @@ describe('EditRelationshipListComponent', () => { relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftLabel: 'isAuthorOfPublication', - rightLabel: 'isPublicationOfAuthor' + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor' }); relationships = [ @@ -119,7 +119,7 @@ describe('EditRelationshipListComponent', () => { de = fixture.debugElement; comp.item = item; comp.url = url; - comp.relationshipLabel = relationshipType.leftLabel; + comp.relationshipLabel = relationshipType.leftwardType; fixture.detectChanges(); }); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts index 3306d8eb01..e98da94c9d 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts @@ -34,8 +34,8 @@ describe('EditRelationshipComponent', () => { relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftLabel: 'isAuthorOfPublication', - rightLabel: 'isPublicationOfAuthor' + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor' }); relationships = [ diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index b1a4e11371..48bc28a1b9 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -68,8 +68,8 @@ describe('ItemRelationshipsComponent', () => { relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftLabel: 'isAuthorOfPublication', - rightLabel: 'isPublicationOfAuthor' + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor' }); relationships = [ diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 75e0a856fa..31513bb779 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -143,7 +143,7 @@ describe('RelationshipService', () => { describe('getRelatedItemsByLabel', () => { it('should return the related items by label', () => { - service.getRelatedItemsByLabel(item, relationshipType.rightLabel).subscribe((result) => { + service.getRelatedItemsByLabel(item, relationshipType.rightwardType).subscribe((result) => { expect(result).toEqual(relatedItems); }); }); From f1148976616b636e473ba46b7d37b0ea46fb93e1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 4 Oct 2019 12:08:30 -0500 Subject: [PATCH 097/110] Minor updates/corrections to README --- docker/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/README.md b/docker/README.md index 17a0dae9c4..f7b4b04848 100644 --- a/docker/README.md +++ b/docker/README.md @@ -24,7 +24,7 @@ docker-compose -f docker/docker-compose.yml pull ## To build DSpace images using code in your branch ``` -docker-compose-f docker/docker-compose.yml build +docker-compose -f docker/docker-compose.yml build ``` ## To start DSpace (REST and Angular) from your branch @@ -72,7 +72,7 @@ docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dsp ``` ## End to end testing of the rest api (runs in travis). -_In this instance, only the REST api runs in Docker. Travis will perform CI testing of Angular using Node to drive the tests._ +_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ ``` docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d From 5b776b605ab34150cc675e07e502e1a19bb59339 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 8 Oct 2019 13:06:31 +0200 Subject: [PATCH 098/110] 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 099/110] 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; From efc91a4591344761620f34c7befa4c1191f331be Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Wed, 25 Sep 2019 17:37:12 -0700 Subject: [PATCH 100/110] Updated work on routing by id. Fixed unit tests. Updated to use pid REST endpoint. Minor change in data.service and unit test update. Updated the objectnotfound page with new text and go home button. --- .../lookup-by-id-routing.module.ts | 19 +++ src/app/+lookup-by-id/lookup-by-id.module.ts | 23 ++++ src/app/+lookup-by-id/lookup-guard.ts | 52 ++++++++ .../objectnotfound.component.html | 8 ++ .../objectnotfound.component.scss | 0 .../objectnotfound.component.ts | 45 +++++++ src/app/app-routing.module.ts | 2 + src/app/app.module.ts | 2 +- src/app/core/cache/object-cache.reducer.ts | 1 + src/app/core/cache/object-cache.service.ts | 16 +-- src/app/core/core.effects.ts | 4 +- src/app/core/data/comcol-data.service.spec.ts | 4 +- src/app/core/data/comcol-data.service.ts | 2 +- src/app/core/data/data.service.ts | 18 ++- .../data/dso-data-redirect.service.spec.ts | 112 ++++++++++++++++++ .../core/data/dso-data-redirect.service.ts | 78 ++++++++++++ .../data/dspace-object-data.service.spec.ts | 3 +- src/app/core/data/request.models.ts | 6 +- src/app/core/data/request.service.ts | 18 ++- src/app/core/index/index.actions.ts | 14 +-- src/app/core/index/index.effects.ts | 38 ++++-- src/app/core/index/index.reducer.spec.ts | 40 +++++-- src/app/core/index/index.reducer.ts | 41 ++++--- src/app/core/index/index.selectors.ts | 34 ++++-- 24 files changed, 498 insertions(+), 82 deletions(-) create mode 100644 src/app/+lookup-by-id/lookup-by-id-routing.module.ts create mode 100644 src/app/+lookup-by-id/lookup-by-id.module.ts create mode 100644 src/app/+lookup-by-id/lookup-guard.ts create mode 100644 src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html create mode 100644 src/app/+lookup-by-id/objectnotfound/objectnotfound.component.scss create mode 100644 src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts create mode 100644 src/app/core/data/dso-data-redirect.service.spec.ts create mode 100644 src/app/core/data/dso-data-redirect.service.ts diff --git a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts new file mode 100644 index 0000000000..012345e791 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts @@ -0,0 +1,19 @@ +import { LookupGuard } from './lookup-guard'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: ':idType/:id', canActivate: [LookupGuard], component: ObjectNotFoundComponent } + ]) + ], + providers: [ + LookupGuard + ] +}) + +export class LookupRoutingModule { + +} diff --git a/src/app/+lookup-by-id/lookup-by-id.module.ts b/src/app/+lookup-by-id/lookup-by-id.module.ts new file mode 100644 index 0000000000..4620f57824 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-by-id.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { LookupRoutingModule } from './lookup-by-id-routing.module'; +import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; +import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; + +@NgModule({ + imports: [ + LookupRoutingModule, + CommonModule, + SharedModule, + ], + declarations: [ + ObjectNotFoundComponent + ], + providers: [ + DsoDataRedirectService + ] +}) +export class LookupIdModule { + +} diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts new file mode 100644 index 0000000000..61e3688ee2 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -0,0 +1,52 @@ +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; +import { Injectable } from '@angular/core'; +import { IdentifierType } from '../core/index/index.reducer'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { RemoteData } from '../core/data/remote-data'; +import { FindByIDRequest } from '../core/data/request.models'; + +interface LookupParams { + type: IdentifierType; + id: string; +} + +@Injectable() +export class LookupGuard implements CanActivate { + constructor(private dsoService: DsoDataRedirectService, private router: Router) { + } + + canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable { + const params = this.getLookupParams(route); + return this.dsoService.findById(params.id, params.type).pipe( + map((response: RemoteData) => response.hasFailed) + ); + } + + private getLookupParams(route: ActivatedRouteSnapshot): LookupParams { + let type; + let id; + const idType = route.params.idType; + + // If the idType is not recognized, assume a legacy handle request (handle/prefix/id) + if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) { + type = IdentifierType.HANDLE; + const prefix = route.params.idType; + const handleId = route.params.id; + id = `${prefix}%2F${handleId}`; + + } else if (route.params.idType === IdentifierType.HANDLE) { + type = IdentifierType.HANDLE; + id = route.params.id; + + } else { + type = IdentifierType.UUID; + id = route.params.id; + } + return { + type: type, + id: id + }; + } +} diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html new file mode 100644 index 0000000000..662d3cde52 --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html @@ -0,0 +1,8 @@ +
+

{{"error.item" | translate}}

+

{{missingItem}}

+
+

+ {{"404.link.home-page" | translate}} +

+
diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.scss b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts new file mode 100644 index 0000000000..0116575154 --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts @@ -0,0 +1,45 @@ + +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** + * This component representing the `PageNotFound` DSpace page. + */ +@Component({ + selector: 'ds-objnotfound', + styleUrls: ['./objectnotfound.component.scss'], + templateUrl: './objectnotfound.component.html', + changeDetection: ChangeDetectionStrategy.Default +}) +export class ObjectNotFoundComponent implements OnInit { + + idType: string; + + id: string; + + missingItem: string; + + /** + * Initialize instance variables + * + * @param {AuthService} authservice + * @param {ServerResponseService} responseService + */ + constructor(private route: ActivatedRoute) { + route.params.subscribe((params) => { + this.idType = params.idType; + this.id = params.id; + }) + } + + ngOnInit(): void { + if (this.idType.startsWith('handle')) { + this.missingItem = 'handle: ' + this.id; + } else if (this.idType.startsWith('uuid')) { + this.missingItem = 'uuid: ' + this.id; + } else { + this.missingItem = 'handle: ' + this.idType + '/' + this.id; + } + } + +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e1ddc2b889..5085633a5b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,6 +27,8 @@ export function getAdminModulePath() { RouterModule.forRoot([ { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' }, + { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, + { path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 916788df8c..3d8bf0ed43 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -128,7 +128,7 @@ const EXPORTS = [ ...PROVIDERS ], declarations: [ - ...DECLARATIONS, + ...DECLARATIONS ], exports: [ ...EXPORTS diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index f41151fd90..afc040bf59 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -44,6 +44,7 @@ export abstract class TypedObject { */ export class CacheableObject extends TypedObject { uuid?: string; + handle?: string; self: string; // isNew: boolean; // dirtyType: DirtyType; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 11f3a6ce3e..95e96db0c8 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; @@ -21,6 +21,7 @@ import { import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { AddToSSBAction } from './server-sync-buffer.actions'; import { getMapsToType } from './builders/build-decorators'; +import { IdentifierType } from '../index/index.reducer'; /** * The base selector function to select the object cache in the store @@ -75,14 +76,15 @@ export class ObjectCacheService { /** * Get an observable of the object with the specified UUID * - * @param uuid + * @param id * The UUID of the object to get * @return Observable> * An observable of the requested object in normalized form */ - getObjectByUUID(uuid: string): Observable> { + getObjectByID(id: string, identifierType: IdentifierType = IdentifierType.UUID): + Observable> { return this.store.pipe( - select(selfLinkFromUuidSelector(uuid)), + select(selfLinkFromUuidSelector(id, identifierType)), mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) ) ) @@ -188,17 +190,17 @@ export class ObjectCacheService { /** * Check whether the object with the specified UUID is cached * - * @param uuid + * @param id * The UUID of the object to check * @return boolean * true if the object with the specified UUID is cached, * false otherwise */ - hasByUUID(uuid: string): boolean { + hasById(id: string, identifierType: IdentifierType = IdentifierType.UUID): boolean { let result: boolean; this.store.pipe( - select(selfLinkFromUuidSelector(uuid)), + select(selfLinkFromUuidSelector(id, identifierType)), take(1) ).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 0eabfc5dc8..ae6f7f3cfa 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,5 +1,5 @@ import { ObjectCacheEffects } from './cache/object-cache.effects'; -import { UUIDIndexEffects } from './index/index.effects'; +import { IdentifierIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; import { AuthEffects } from './auth/auth.effects'; import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; @@ -10,7 +10,7 @@ import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, ObjectCacheEffects, - UUIDIndexEffects, + IdentifierIndexEffects, AuthEffects, JsonPatchOperationsEffects, ServerSyncBufferEffects, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5232b0bff..de17d7a39f 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -95,7 +95,7 @@ describe('ComColDataService', () => { function initMockObjectCacheService(): ObjectCacheService { return jasmine.createSpyObj('objectCache', { - getObjectByUUID: cold('d-', { + getObjectByID: cold('d-', { d: { _links: { [LINK_NAME]: scopedEndpoint @@ -160,7 +160,7 @@ describe('ComColDataService', () => { it('should fetch the scope Community from the cache', () => { scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.flush(); - expect(objectCache.getObjectByUUID).toHaveBeenCalledWith(scopeID); + expect(objectCache.getObjectByID).toHaveBeenCalledWith(scopeID); }); it('should return the endpoint to fetch resources within the given scope', () => { diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 68eb3e4880..3059d568df 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -49,7 +49,7 @@ export abstract class ComColDataService extends DataS ); const successResponses = responses.pipe( filter((response) => response.isSuccessful), - mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)), + mergeMap(() => this.objectCache.getObjectByID(options.scopeID)), map((nc: NormalizedCommunity) => nc._links[linkPath]), filter((href) => isNotEmpty(href)) ); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ad0db51980..0f7ca74d15 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -37,6 +37,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ChangeAnalyzer } from './change-analyzer'; import { RestRequestMethod } from './rest-request-method'; import { getMapsToType } from '../cache/builders/build-decorators'; +import { IdentifierType } from '../index/index.reducer'; export abstract class DataService { protected abstract requestService: RequestService; @@ -146,14 +147,21 @@ export abstract class DataService { return `${endpoint}/${resourceID}`; } - findById(id: string): Observable> { - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, id))); - + findById(id: string, identifierType: IdentifierType = IdentifierType.UUID): Observable> { + let hrefObs; + if (identifierType === IdentifierType.UUID) { + hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, id))); + } else if (identifierType === IdentifierType.HANDLE) { + hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => { + return this.getIDHref(endpoint, encodeURIComponent(id)); + })); + } hrefObs.pipe( find((href: string) => hasValue(href))) .subscribe((href: string) => { - const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); + const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id, identifierType); this.requestService.configure(request, this.forceBypassCache); }); diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-data-redirect.service.spec.ts new file mode 100644 index 0000000000..ece3c242fc --- /dev/null +++ b/src/app/core/data/dso-data-redirect.service.spec.ts @@ -0,0 +1,112 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { FindByIDRequest } from './request.models'; +import { RequestService } from './request.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { IdentifierType } from '../index/index.reducer'; +import { DsoDataRedirectService } from './dso-data-redirect.service'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; + +describe('DsoDataRedirectService', () => { + let scheduler: TestScheduler; + let service: DsoDataRedirectService; + let halService: HALEndpointService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let router: Router; + let remoteData; + const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746'; + const dsoHandle = '1234567789/22'; + const encodedHandle = encodeURIComponent(dsoHandle); + const pidLink = 'https://rest.api/rest/api/pid/find{?id}'; + const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`; + const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`; + const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; + const testObject = { + uuid: dsoUUID + } as DSpaceObject; + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: pidLink }) + }); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: { + payload: testObject + } + }) + }); + router = jasmine.createSpyObj('router', { + navigate: () => true + }); + remoteData = { + isSuccessful: true, + error: undefined, + hasSucceeded: true, + payload: { + type: 'item', + id: '123456789' + } + }; + objectCache = {} as ObjectCacheService; + const store = {} as Store; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + + service = new DsoDataRedirectService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + router + ); + }); + + describe('findById', () => { + it('should call HALEndpointService with the path to the dso endpoint', () => { + scheduler.schedule(() => service.findById(dsoUUID)); + scheduler.flush(); + + expect(halService.getEndpoint).toHaveBeenCalledWith('pid'); + }); + + it('should configure the proper FindByIDRequest for uuid', () => { + scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID, IdentifierType.UUID), false); + }); + + it('should configure the proper FindByIDRequest for handle', () => { + scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle, IdentifierType.HANDLE), false); + }); + + // TODO: test for router.navigate + }); +}); diff --git a/src/app/core/data/dso-data-redirect.service.ts b/src/app/core/data/dso-data-redirect.service.ts new file mode 100644 index 0000000000..b568a23c8a --- /dev/null +++ b/src/app/core/data/dso-data-redirect.service.ts @@ -0,0 +1,78 @@ +import { DataService } from './data.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestService } from './request.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { FindAllOptions, FindByIDRequest } from './request.models'; +import { Observable, of } from 'rxjs'; +import { IdentifierType } from '../index/index.reducer'; +import { RemoteData } from './remote-data'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { Injectable } from '@angular/core'; +import { map, tap } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { getFinishedRemoteData, getSucceededRemoteData } from '../shared/operators'; +import { Router } from '@angular/router'; + +@Injectable() +export class DsoDataRedirectService extends DataService { + + protected linkPath = 'pid'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + private router: Router) { + super(); + } + + getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } + + getIDHref(endpoint, resourceID): string { + return endpoint.replace(/\{\?id\}/,`?id=${resourceID}`); + } + + findById(id: string, identifierType = IdentifierType.UUID): Observable> { + return super.findById(id, identifierType).pipe( + getFinishedRemoteData(), + tap((response) => { + if (response.hasSucceeded) { + const uuid = response.payload.uuid; + // Is there an existing method somewhere that converts dso type to endpoint? + // This will not work for all endpoints! + const dsoType = this.getEndpointFromDSOType(response.payload.type); + if (hasValue(uuid) && hasValue(dsoType)) { + this.router.navigate([dsoType + '/' + uuid]); + } + } + }) + ); + } + + getEndpointFromDSOType(dsoType: string): string { + if (dsoType.startsWith('item')) { + return 'items' + } else if (dsoType.startsWith('community')) { + return 'communities'; + } else if (dsoType.startsWith('collection')) { + return 'collections' + } else { + return ''; + } + } +} diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index a0bba214ae..b411028420 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -10,6 +10,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { IdentifierType } from '../index/index.reducer'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -72,7 +73,7 @@ describe('DSpaceObjectDataService', () => { scheduler.schedule(() => service.findById(testObject.uuid)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid), false); + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid, IdentifierType.UUID), false); }); it('should return a RemoteData for the object with the given ID', () => { diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 43ab9e58e7..45e8fdea16 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -19,6 +19,7 @@ import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service'; +import { IdentifierType } from '../index/index.reducer'; /* tslint:disable:max-classes-per-file */ @@ -49,7 +50,7 @@ export class GetRequest extends RestRequest { public uuid: string, public href: string, public body?: any, - public options?: HttpOptions, + public options?: HttpOptions ) { super(uuid, href, RestRequestMethod.GET, body, options) } @@ -125,7 +126,8 @@ export class FindByIDRequest extends GetRequest { constructor( uuid: string, href: string, - public resourceID: string + public resourceID: string, + public identifierType?: IdentifierType ) { super(uuid, href); } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 0980d48537..ac65042238 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -11,7 +11,7 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; -import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer'; +import { IdentifierType, IndexState, MetaIndexState, REQUEST, UUID_MAPPING } from '../index/index.reducer'; import { originalRequestUUIDFromRequestUUIDSelector, requestIndexSelector, @@ -19,7 +19,7 @@ import { } from '../index/index.selectors'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; -import { GetRequest, RestRequest } from './request.models'; +import { FindByIDRequest, GetRequest, RestRequest } from './request.models'; import { RequestEntry, RequestState } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; @@ -162,7 +162,7 @@ export class RequestService { filter((entry) => hasValue(entry)), take(1) ).subscribe((entry) => { - return this.store.dispatch(new AddToIndexAction(IndexName.UUID_MAPPING, request.uuid, entry.request.uuid)) + return this.store.dispatch(new AddToIndexAction(UUID_MAPPING, request.uuid, entry.request.uuid)) } ) } @@ -206,7 +206,7 @@ export class RequestService { } }); this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0); - this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); + this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(REQUEST, href)); } /** @@ -225,8 +225,14 @@ export class RequestService { private isCachedOrPending(request: GetRequest): boolean { const inReqCache = this.hasByHref(request.href); const inObjCache = this.objectCache.hasBySelfLink(request.href); - const isCached = inReqCache || inObjCache; - + let inObjIdCache = false; + if (request instanceof FindByIDRequest) { + const req = request as FindByIDRequest; + if (hasValue(req.identifierType && hasValue(req.resourceID))) { + inObjIdCache = this.objectCache.hasById(req.resourceID, req.identifierType) + } + } + const isCached = inReqCache || inObjCache || inObjIdCache; const isPending = this.isPending(request); return isCached || isPending; } diff --git a/src/app/core/index/index.actions.ts b/src/app/core/index/index.actions.ts index 42804dbe26..24f031b33c 100644 --- a/src/app/core/index/index.actions.ts +++ b/src/app/core/index/index.actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; -import { IndexName } from './index.reducer'; +import { } from './index.reducer'; /** * The list of HrefIndexAction type definitions @@ -19,7 +19,7 @@ export const IndexActionTypes = { export class AddToIndexAction implements Action { type = IndexActionTypes.ADD; payload: { - name: IndexName; + name: string; value: string; key: string; }; @@ -34,7 +34,7 @@ export class AddToIndexAction implements Action { * @param value * the self link of the resource the key belongs to */ - constructor(name: IndexName, key: string, value: string) { + constructor(name: string, key: string, value: string) { this.payload = { name, key, value }; } } @@ -45,7 +45,7 @@ export class AddToIndexAction implements Action { export class RemoveFromIndexByValueAction implements Action { type = IndexActionTypes.REMOVE_BY_VALUE; payload: { - name: IndexName, + name: string, value: string }; @@ -57,7 +57,7 @@ export class RemoveFromIndexByValueAction implements Action { * @param value * the value to remove the UUID for */ - constructor(name: IndexName, value: string) { + constructor(name: string, value: string) { this.payload = { name, value }; } @@ -69,7 +69,7 @@ export class RemoveFromIndexByValueAction implements Action { export class RemoveFromIndexBySubstringAction implements Action { type = IndexActionTypes.REMOVE_BY_SUBSTRING; payload: { - name: IndexName, + name: string, value: string }; @@ -81,7 +81,7 @@ export class RemoveFromIndexBySubstringAction implements Action { * @param value * the value to remove the UUID for */ - constructor(name: IndexName, value: string) { + constructor(name: string, value: string) { this.payload = { name, value }; } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 61cf313ab1..0cdf6bea6c 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -10,43 +10,67 @@ import { import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions'; import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; import { hasValue } from '../../shared/empty.util'; -import { IndexName } from './index.reducer'; +import { getIdentiferByIndexName, IdentifierType, REQUEST } from './index.reducer'; import { RestRequestMethod } from '../data/rest-request-method'; @Injectable() -export class UUIDIndexEffects { +export class IdentifierIndexEffects { - @Effect() addObject$ = this.actions$ + @Effect() addObjectByUUID$ = this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( - IndexName.OBJECT, + getIdentiferByIndexName(IdentifierType.UUID), action.payload.objectToCache.uuid, action.payload.objectToCache.self ); }) ); - @Effect() removeObject$ = this.actions$ + @Effect() addObjectByHandle$ = this.actions$ + .pipe( + ofType(ObjectCacheActionTypes.ADD), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.handle)), + map((action: AddToObjectCacheAction) => { + return new AddToIndexAction( + getIdentiferByIndexName(IdentifierType.HANDLE), + action.payload.objectToCache.handle, + action.payload.objectToCache.self + ); + }) + ); + + @Effect() removeObjectByUUID$ = this.actions$ .pipe( ofType(ObjectCacheActionTypes.REMOVE), map((action: RemoveFromObjectCacheAction) => { return new RemoveFromIndexByValueAction( - IndexName.OBJECT, + getIdentiferByIndexName(IdentifierType.UUID), action.payload ); }) ); + @Effect() removeObjectByHandle$ = this.actions$ + .pipe( + ofType(ObjectCacheActionTypes.REMOVE), + map((action: RemoveFromObjectCacheAction) => { + return new RemoveFromIndexByValueAction( + getIdentiferByIndexName(IdentifierType.HANDLE), + action.payload + ); + }) + ); + @Effect() addRequest$ = this.actions$ .pipe( ofType(RequestActionTypes.CONFIGURE), filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.GET), map((action: RequestConfigureAction) => { return new AddToIndexAction( - IndexName.REQUEST, + REQUEST, action.payload.href, action.payload.uuid ); diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts index ef46c760c6..35460a9ef5 100644 --- a/src/app/core/index/index.reducer.spec.ts +++ b/src/app/core/index/index.reducer.spec.ts @@ -1,6 +1,6 @@ import * as deepFreeze from 'deep-freeze'; -import { IndexName, indexReducer, MetaIndexState } from './index.reducer'; +import { getIdentiferByIndexName, IdentifierType, indexReducer, MetaIndexState, REQUEST, } from './index.reducer'; import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions'; class NullAction extends AddToIndexAction { @@ -15,14 +15,19 @@ class NullAction extends AddToIndexAction { describe('requestReducer', () => { const key1 = '567a639f-f5ff-4126-807c-b7d0910808c8'; const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const key3 = '123456789/22'; const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const uuidIndex = getIdentiferByIndexName(IdentifierType.UUID); + const handleIndex = getIdentiferByIndexName(IdentifierType.HANDLE); const testState: MetaIndexState = { - [IndexName.OBJECT]: { + 'object/uuid-to-self-link/uuid': { [key1]: val1 - },[IndexName.REQUEST]: { + },'object/uuid-to-self-link/handle': { + [key3]: val1 + },'get-request/href-to-uuid': { [key1]: val1 - },[IndexName.UUID_MAPPING]: { + },'get-request/configured-to-cache-uuid': { [key1]: val1 } }; @@ -45,27 +50,38 @@ describe('requestReducer', () => { it('should add the \'key\' with the corresponding \'value\' to the correct substate, in response to an ADD action', () => { const state = testState; - const action = new AddToIndexAction(IndexName.REQUEST, key2, val2); + const action = new AddToIndexAction(REQUEST, key2, val2); const newState = indexReducer(state, action); - expect(newState[IndexName.REQUEST][key2]).toEqual(val2); + expect(newState[REQUEST][key2]).toEqual(val2); }); it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_VALUE action', () => { const state = testState; - const action = new RemoveFromIndexByValueAction(IndexName.OBJECT, val1); - const newState = indexReducer(state, action); + let action = new RemoveFromIndexByValueAction(uuidIndex, val1); + let newState = indexReducer(state, action); + + expect(newState[uuidIndex][key1]).toBeUndefined(); + + action = new RemoveFromIndexByValueAction(handleIndex, val1); + newState = indexReducer(state, action); + + expect(newState[handleIndex][key3]).toBeUndefined(); - expect(newState[IndexName.OBJECT][key1]).toBeUndefined(); }); it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_SUBSTRING action', () => { const state = testState; - const action = new RemoveFromIndexBySubstringAction(IndexName.OBJECT, key1); - const newState = indexReducer(state, action); + let action = new RemoveFromIndexBySubstringAction(uuidIndex, key1); + let newState = indexReducer(state, action); - expect(newState[IndexName.OBJECT][key1]).toBeUndefined(); + expect(newState[uuidIndex][key1]).toBeUndefined(); + + action = new RemoveFromIndexBySubstringAction(handleIndex, key3); + newState = indexReducer(state, action); + + expect(newState[uuidIndex][key3]).toBeUndefined(); }); }); diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts index b4cd8aa84b..631d579911 100644 --- a/src/app/core/index/index.reducer.ts +++ b/src/app/core/index/index.reducer.ts @@ -6,24 +6,26 @@ import { RemoveFromIndexByValueAction } from './index.actions'; -/** - * An enum containing all index names - */ -export enum IndexName { - // Contains all objects in the object cache indexed by UUID - OBJECT = 'object/uuid-to-self-link', - - // contains all requests in the request cache indexed by UUID - REQUEST = 'get-request/href-to-uuid', - - /** - * Contains the UUIDs of requests that were sent to the server and - * have their responses cached, indexed by the UUIDs of requests that - * weren't sent because the response they requested was already cached - */ - UUID_MAPPING = 'get-request/configured-to-cache-uuid' +export enum IdentifierType { + UUID ='uuid', + HANDLE = 'handle' } +/** + * Contains the UUIDs of requests that were sent to the server and + * have their responses cached, indexed by the UUIDs of requests that + * weren't sent because the response they requested was already cached + */ +export const UUID_MAPPING = 'get-request/configured-to-cache-uuid'; + +// contains all requests in the request cache indexed by UUID +export const REQUEST = 'get-request/href-to-uuid'; + +// returns the index for the provided id type (uuid, handle) +export const getIdentiferByIndexName = (idType: IdentifierType): string => { + return `object/uuid-to-self-link/${idType}`; +}; + /** * The state of a single index */ @@ -34,8 +36,11 @@ export interface IndexState { /** * The state that contains all indices */ -export type MetaIndexState = { - [name in IndexName]: IndexState +export interface MetaIndexState { + 'get-request/configured-to-cache-uuid': IndexState, + 'get-request/href-to-uuid': IndexState, + 'object/uuid-to-self-link/uuid': IndexState, + 'object/uuid-to-self-link/handle': IndexState } // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts index 3c7b331a92..80a7c0d46a 100644 --- a/src/app/core/index/index.selectors.ts +++ b/src/app/core/index/index.selectors.ts @@ -3,7 +3,14 @@ import { AppState } from '../../app.reducer'; import { hasValue } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; -import { IndexName, IndexState, MetaIndexState } from './index.reducer'; +import { + getIdentiferByIndexName, + IdentifierType, + IndexState, + MetaIndexState, + REQUEST, + UUID_MAPPING +} from './index.reducer'; /** * Return the MetaIndexState based on the CoreSate @@ -20,13 +27,17 @@ export const metaIndexSelector: MemoizedSelector = cre * Return the object index based on the MetaIndexState * It contains all objects in the object cache indexed by UUID * + * @param identifierType the type of index, used to select index from state + * * @returns * a MemoizedSelector to select the object index */ -export const objectIndexSelector: MemoizedSelector = createSelector( - metaIndexSelector, - (state: MetaIndexState) => state[IndexName.OBJECT] -); +export const objectIndexSelector = (identifierType: IdentifierType = IdentifierType.UUID): MemoizedSelector => { + return createSelector( + metaIndexSelector, + (state: MetaIndexState) => state[getIdentiferByIndexName(identifierType)] + ); +} /** * Return the request index based on the MetaIndexState @@ -36,7 +47,7 @@ export const objectIndexSelector: MemoizedSelector = creat */ export const requestIndexSelector: MemoizedSelector = createSelector( metaIndexSelector, - (state: MetaIndexState) => state[IndexName.REQUEST] + (state: MetaIndexState) => state[REQUEST] ); /** @@ -47,21 +58,22 @@ export const requestIndexSelector: MemoizedSelector = crea */ export const requestUUIDIndexSelector: MemoizedSelector = createSelector( metaIndexSelector, - (state: MetaIndexState) => state[IndexName.UUID_MAPPING] + (state: MetaIndexState) => state[UUID_MAPPING] ); /** * Return the self link of an object in the object-cache based on its UUID * - * @param uuid + * @param id * the UUID for which you want to find the matching self link + * @param identifierType the type of index, used to select index from state * @returns * a MemoizedSelector to select the self link */ export const selfLinkFromUuidSelector = - (uuid: string): MemoizedSelector => createSelector( - objectIndexSelector, - (state: IndexState) => hasValue(state) ? state[uuid] : undefined + (id: string, identifierType: IdentifierType = IdentifierType.UUID): MemoizedSelector => createSelector( + objectIndexSelector(identifierType), + (state: IndexState) => hasValue(state) ? state[id] : undefined ); /** From 5ec9b7c29f4c90f1604613e123d140e14b4150a9 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Mon, 30 Sep 2019 16:57:06 -0700 Subject: [PATCH 101/110] Added new unit tests. --- .../objectnotfound.component.spec.ts | 79 +++++++++++++++++++ .../data/dso-data-redirect.service.spec.ts | 35 ++++---- .../core/data/dso-data-redirect.service.ts | 6 +- 3 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 src/app/+lookup-by-id/objectnotfound/objectnotfound.component.spec.ts diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.spec.ts b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.spec.ts new file mode 100644 index 0000000000..7905655a06 --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.spec.ts @@ -0,0 +1,79 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ObjectNotFoundComponent } from './objectnotfound.component'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; +import { of as observableOf } from 'rxjs'; +import { ActivatedRoute } from '@angular/router'; + +describe('ObjectNotFoundComponent', () => { + let comp: ObjectNotFoundComponent; + let fixture: ComponentFixture; + const testUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; + const uuidType = 'uuid'; + const handlePrefix = '123456789'; + const handleId = '22'; + const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { + params: observableOf({id: testUUID, idType: uuidType}) + }); + const activatedRouteStubHandle = Object.assign(new ActivatedRouteStub(), { + params: observableOf({id: handleId, idType: handlePrefix}) + }); + describe('uuid request', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], providers: [ + {provide: ActivatedRoute, useValue: activatedRouteStub} + ], + declarations: [ObjectNotFoundComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ObjectNotFoundComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create instance', () => { + expect(comp).toBeDefined() + }); + + it('should have id and idType', () => { + expect(comp.id).toEqual(testUUID); + expect(comp.idType).toEqual(uuidType); + expect(comp.missingItem).toEqual('uuid: ' + testUUID); + }); + }); + + describe( 'legacy handle request', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], providers: [ + {provide: ActivatedRoute, useValue: activatedRouteStubHandle} + ], + declarations: [ObjectNotFoundComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ObjectNotFoundComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should have handle prefix and id', () => { + expect(comp.id).toEqual(handleId); + expect(comp.idType).toEqual(handlePrefix); + expect(comp.missingItem).toEqual('handle: ' + handlePrefix + '/' + handleId); + }); + }); + +}); diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-data-redirect.service.spec.ts index ece3c242fc..040cecc435 100644 --- a/src/app/core/data/dso-data-redirect.service.spec.ts +++ b/src/app/core/data/dso-data-redirect.service.spec.ts @@ -1,7 +1,6 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; @@ -11,7 +10,6 @@ import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { IdentifierType } from '../index/index.reducer'; import { DsoDataRedirectService } from './dso-data-redirect.service'; -import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; @@ -22,7 +20,7 @@ describe('DsoDataRedirectService', () => { let requestService: RequestService; let rdbService: RemoteDataBuildService; let objectCache: ObjectCacheService; - let router: Router; + let router; let remoteData; const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746'; const dsoHandle = '1234567789/22'; @@ -31,15 +29,12 @@ describe('DsoDataRedirectService', () => { const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`; const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`; const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; - const testObject = { - uuid: dsoUUID - } as DSpaceObject; beforeEach(() => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: pidLink }) + getEndpoint: cold('a', {a: pidLink}) }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, @@ -47,21 +42,20 @@ describe('DsoDataRedirectService', () => { }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { - a: { - payload: testObject - } + a: remoteData }) }); - router = jasmine.createSpyObj('router', { - navigate: () => true - }); + router = { + navigate: jasmine.createSpy('navigate') + }; remoteData = { isSuccessful: true, error: undefined, hasSucceeded: true, + isLoading: false, payload: { type: 'item', - id: '123456789' + uuid: '123456789' } }; objectCache = {} as ObjectCacheService; @@ -86,7 +80,7 @@ describe('DsoDataRedirectService', () => { }); describe('findById', () => { - it('should call HALEndpointService with the path to the dso endpoint', () => { + it('should call HALEndpointService with the path to the pid endpoint', () => { scheduler.schedule(() => service.findById(dsoUUID)); scheduler.flush(); @@ -107,6 +101,13 @@ describe('DsoDataRedirectService', () => { expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle, IdentifierType.HANDLE), false); }); - // TODO: test for router.navigate - }); + it('should navigate to dso route', () => { + const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + // The framework would normally subscribe but do it here so we can test navigation. + redir.subscribe(); + scheduler.schedule(() => redir); + scheduler.flush(); + expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]); + }); + }) }); diff --git a/src/app/core/data/dso-data-redirect.service.ts b/src/app/core/data/dso-data-redirect.service.ts index b568a23c8a..3d779de5cd 100644 --- a/src/app/core/data/dso-data-redirect.service.ts +++ b/src/app/core/data/dso-data-redirect.service.ts @@ -9,14 +9,14 @@ import { RequestService } from './request.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; import { FindAllOptions, FindByIDRequest } from './request.models'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { IdentifierType } from '../index/index.reducer'; import { RemoteData } from './remote-data'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { Injectable } from '@angular/core'; -import { map, tap } from 'rxjs/operators'; +import { tap } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; -import { getFinishedRemoteData, getSucceededRemoteData } from '../shared/operators'; +import { getFinishedRemoteData } from '../shared/operators'; import { Router } from '@angular/router'; @Injectable() From a1d21cd6af79d5bcf82d0ad11df632dbf48abcb5 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Tue, 1 Oct 2019 01:06:12 -0700 Subject: [PATCH 102/110] Minor change to comment. --- src/app/core/data/dso-data-redirect.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/dso-data-redirect.service.ts b/src/app/core/data/dso-data-redirect.service.ts index 3d779de5cd..1cb3b20f2a 100644 --- a/src/app/core/data/dso-data-redirect.service.ts +++ b/src/app/core/data/dso-data-redirect.service.ts @@ -53,8 +53,7 @@ export class DsoDataRedirectService extends DataService { tap((response) => { if (response.hasSucceeded) { const uuid = response.payload.uuid; - // Is there an existing method somewhere that converts dso type to endpoint? - // This will not work for all endpoints! + // Is there an existing method somewhere that converts dso type route? const dsoType = this.getEndpointFromDSOType(response.payload.type); if (hasValue(uuid) && hasValue(dsoType)) { this.router.navigate([dsoType + '/' + uuid]); @@ -65,6 +64,7 @@ export class DsoDataRedirectService extends DataService { } getEndpointFromDSOType(dsoType: string): string { + // Are there other routes to consider? if (dsoType.startsWith('item')) { return 'items' } else if (dsoType.startsWith('community')) { From 2aaab8342773e63b5db6314e603b36a0d159a170 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Tue, 1 Oct 2019 03:03:08 -0700 Subject: [PATCH 103/110] Added unit tests. --- src/app/+lookup-by-id/lookup-guard.spec.ts | 50 ++++++++++++ src/app/+lookup-by-id/lookup-guard.ts | 2 +- .../data/dso-data-redirect.service.spec.ts | 81 +++++++++++++------ 3 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/app/+lookup-by-id/lookup-guard.spec.ts diff --git a/src/app/+lookup-by-id/lookup-guard.spec.ts b/src/app/+lookup-by-id/lookup-guard.spec.ts new file mode 100644 index 0000000000..ea077d0811 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-guard.spec.ts @@ -0,0 +1,50 @@ +import {LookupGuard} from "./lookup-guard"; +import {of as observableOf} from "rxjs"; +import {IdentifierType} from "../core/index/index.reducer"; + +describe('LookupGuard', () => { + let dsoService: any; + let guard: any; + + beforeEach(() => { + dsoService = { + findById: jasmine.createSpy('findById').and.returnValue(observableOf({ hasFailed: false, + hasSucceeded: true })) + }; + guard = new LookupGuard(dsoService); + }); + + it('should call findById with handle params', () => { + const scopedRoute = { + params: { + id: '1234', + idType: '123456789' + } + }; + guard.canActivate(scopedRoute as any, undefined); + expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE) + }); + + it('should call findById with handle params', () => { + const scopedRoute = { + params: { + id: '123456789%2F1234', + idType: 'handle' + } + }; + guard.canActivate(scopedRoute as any, undefined); + expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE) + }); + + it('should call findById with UUID params', () => { + const scopedRoute = { + params: { + id: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', + idType: 'uuid' + } + }; + guard.canActivate(scopedRoute as any, undefined); + expect(dsoService.findById).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID) + }); + +}); diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index 61e3688ee2..ceb11b7cf5 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -14,7 +14,7 @@ interface LookupParams { @Injectable() export class LookupGuard implements CanActivate { - constructor(private dsoService: DsoDataRedirectService, private router: Router) { + constructor(private dsoService: DsoDataRedirectService) { } canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable { diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-data-redirect.service.spec.ts index 040cecc435..a9a83e72d6 100644 --- a/src/app/core/data/dso-data-redirect.service.spec.ts +++ b/src/app/core/data/dso-data-redirect.service.spec.ts @@ -19,7 +19,6 @@ describe('DsoDataRedirectService', () => { let halService: HALEndpointService; let requestService: RequestService; let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; let router; let remoteData; const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746'; @@ -29,7 +28,13 @@ describe('DsoDataRedirectService', () => { const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`; const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`; const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; - + const store = {} as Store; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + const objectCache = {} as ObjectCacheService; + let setup; beforeEach(() => { scheduler = getTestScheduler(); @@ -40,14 +45,10 @@ describe('DsoDataRedirectService', () => { generateRequestId: requestUUID, configure: true }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: cold('a', { - a: remoteData - }) - }); router = { navigate: jasmine.createSpy('navigate') }; + remoteData = { isSuccessful: true, error: undefined, @@ -58,29 +59,31 @@ describe('DsoDataRedirectService', () => { uuid: '123456789' } }; - objectCache = {} as ObjectCacheService; - const store = {} as Store; - const notificationsService = {} as NotificationsService; - const http = {} as HttpClient; - const comparator = {} as any; - const dataBuildService = {} as NormalizedObjectBuildService; - service = new DsoDataRedirectService( - requestService, - rdbService, - dataBuildService, - store, - objectCache, - halService, - notificationsService, - http, - comparator, - router - ); + setup = () => { + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: remoteData + }) + }); + service = new DsoDataRedirectService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + router + ); + } }); describe('findById', () => { it('should call HALEndpointService with the path to the pid endpoint', () => { + setup(); scheduler.schedule(() => service.findById(dsoUUID)); scheduler.flush(); @@ -88,6 +91,7 @@ describe('DsoDataRedirectService', () => { }); it('should configure the proper FindByIDRequest for uuid', () => { + setup(); scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); scheduler.flush(); @@ -95,13 +99,16 @@ describe('DsoDataRedirectService', () => { }); it('should configure the proper FindByIDRequest for handle', () => { + setup(); scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle, IdentifierType.HANDLE), false); }); - it('should navigate to dso route', () => { + it('should navigate to item route', () => { + remoteData.payload.type = 'item'; + setup(); const redir = service.findById(dsoHandle, IdentifierType.HANDLE); // The framework would normally subscribe but do it here so we can test navigation. redir.subscribe(); @@ -109,5 +116,27 @@ describe('DsoDataRedirectService', () => { scheduler.flush(); expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]); }); + + it('should navigate to collections route', () => { + remoteData.payload.type = 'collection'; + setup(); + const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + redir.subscribe(); + scheduler.schedule(() => redir); + scheduler.flush(); + expect(router.navigate).toHaveBeenCalledWith([remoteData.payload.type + 's/' + remoteData.payload.uuid]); + }); + + it('should navigate to communities route', () => { + remoteData.payload.type = 'community'; + setup(); + const redir = service.findById(dsoHandle, IdentifierType.HANDLE); + redir.subscribe(); + scheduler.schedule(() => redir); + scheduler.flush(); + expect(router.navigate).toHaveBeenCalledWith(['communities/' + remoteData.payload.uuid]); + }); }) }); + + From 5e40d7a4c167d8bd30d84d36f6c8e92567f23024 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Tue, 1 Oct 2019 03:38:56 -0700 Subject: [PATCH 104/110] Fixed lint errors. --- src/app/+lookup-by-id/lookup-guard.spec.ts | 6 +++--- src/app/core/data/dso-data-redirect.service.spec.ts | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/+lookup-by-id/lookup-guard.spec.ts b/src/app/+lookup-by-id/lookup-guard.spec.ts index ea077d0811..7b00383783 100644 --- a/src/app/+lookup-by-id/lookup-guard.spec.ts +++ b/src/app/+lookup-by-id/lookup-guard.spec.ts @@ -1,6 +1,6 @@ -import {LookupGuard} from "./lookup-guard"; -import {of as observableOf} from "rxjs"; -import {IdentifierType} from "../core/index/index.reducer"; +import { LookupGuard } from './lookup-guard'; +import { of as observableOf } from 'rxjs'; +import { IdentifierType } from '../core/index/index.reducer'; describe('LookupGuard', () => { let dsoService: any; diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-data-redirect.service.spec.ts index a9a83e72d6..f2c35727fa 100644 --- a/src/app/core/data/dso-data-redirect.service.spec.ts +++ b/src/app/core/data/dso-data-redirect.service.spec.ts @@ -138,5 +138,3 @@ describe('DsoDataRedirectService', () => { }); }) }); - - From 2d49f3e765e73c1d838813d19406698a87d50436 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Tue, 1 Oct 2019 12:25:41 -0700 Subject: [PATCH 105/110] Added unit test for isCachedOrPending lookup by id (FindByIdRequest). --- src/app/core/data/request.service.spec.ts | 14 +++++++++++++- src/app/shared/mocks/mock-object-cache.service.ts | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index e2bc04040f..5ceca8b4bd 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -11,6 +11,7 @@ import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { DeleteRequest, + FindByIDRequest, GetRequest, HeadRequest, OptionsRequest, @@ -21,6 +22,7 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { TestScheduler } from 'rxjs/testing'; +import { IdentifierType } from '../index/index.reducer'; describe('RequestService', () => { let scheduler: TestScheduler; @@ -38,6 +40,7 @@ describe('RequestService', () => { const testDeleteRequest = new DeleteRequest(testUUID, testHref); const testOptionsRequest = new OptionsRequest(testUUID, testHref); const testHeadRequest = new HeadRequest(testUUID, testHref); + const testFindByIdRequest = new FindByIDRequest(testUUID, testHref, testUUID, IdentifierType.UUID); const testPatchRequest = new PatchRequest(testUUID, testHref); let selectSpy; @@ -298,13 +301,22 @@ describe('RequestService', () => { describe('in the ObjectCache', () => { beforeEach(() => { (objectCache.hasBySelfLink as any).and.returnValue(true); + (objectCache.hasById as any).and.returnValue(true); spyOn(serviceAsAny, 'hasByHref').and.returnValue(false); }); - it('should return true', () => { + it('should return true for GetRequest', () => { const result = serviceAsAny.isCachedOrPending(testGetRequest); const expected = true; + expect(result).toEqual(expected); + }); + it('should return true for instance of FindByIdRequest', () => { + (objectCache.hasBySelfLink as any).and.returnValue(false); + const result = serviceAsAny.isCachedOrPending(testFindByIdRequest); + expect(objectCache.hasById).toHaveBeenCalledWith(testUUID, IdentifierType.UUID); + const expected = true; + expect(result).toEqual(expected); }); }); diff --git a/src/app/shared/mocks/mock-object-cache.service.ts b/src/app/shared/mocks/mock-object-cache.service.ts index 9e35a519ff..d096cfdf5f 100644 --- a/src/app/shared/mocks/mock-object-cache.service.ts +++ b/src/app/shared/mocks/mock-object-cache.service.ts @@ -4,12 +4,12 @@ export function getMockObjectCacheService(): ObjectCacheService { return jasmine.createSpyObj('objectCacheService', [ 'add', 'remove', - 'getByUUID', + 'getByID', 'getBySelfLink', 'getRequestHrefBySelfLink', 'getRequestHrefByUUID', 'getList', - 'hasByUUID', + 'hasById', 'hasBySelfLink' ]); From 85d179e27fcc4db74061d2b07af64171e4d975c9 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Tue, 15 Oct 2019 11:18:06 -0700 Subject: [PATCH 106/110] Bugfix for request by handle (removed unnecessary encoding) --- src/app/+lookup-by-id/lookup-guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index ceb11b7cf5..7f38b6db7a 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -34,7 +34,7 @@ export class LookupGuard implements CanActivate { type = IdentifierType.HANDLE; const prefix = route.params.idType; const handleId = route.params.id; - id = `${prefix}%2F${handleId}`; + id = `${prefix}/${handleId}`; } else if (route.params.idType === IdentifierType.HANDLE) { type = IdentifierType.HANDLE; From b695da84878429076c68f497f047fcf7f10f91a5 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Wed, 16 Oct 2019 23:39:47 -0700 Subject: [PATCH 107/110] Encoding removed. --- src/app/+lookup-by-id/lookup-guard.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+lookup-by-id/lookup-guard.spec.ts b/src/app/+lookup-by-id/lookup-guard.spec.ts index 7b00383783..824e31c62b 100644 --- a/src/app/+lookup-by-id/lookup-guard.spec.ts +++ b/src/app/+lookup-by-id/lookup-guard.spec.ts @@ -22,7 +22,7 @@ describe('LookupGuard', () => { } }; guard.canActivate(scopedRoute as any, undefined); - expect(dsoService.findById).toHaveBeenCalledWith('123456789%2F1234', IdentifierType.HANDLE) + expect(dsoService.findById).toHaveBeenCalledWith('123456789/1234', IdentifierType.HANDLE) }); it('should call findById with handle params', () => { From 6b26506d881910b2e18ad4d4e893de340341d3dc Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Thu, 17 Oct 2019 14:55:45 -0700 Subject: [PATCH 108/110] Added matcher function to router that supports handle paths (eliminates the need to encode the forward slash). Fixed comment. --- .../lookup-by-id-routing.module.ts | 28 +++++++++++++++++-- src/app/+lookup-by-id/lookup-by-id.module.ts | 4 +-- src/app/+lookup-by-id/lookup-guard.ts | 4 +-- ...c.ts => dso-redirect-data.service.spec.ts} | 8 +++--- ...ervice.ts => dso-redirect-data.service.ts} | 2 +- 5 files changed, 35 insertions(+), 11 deletions(-) rename src/app/core/data/{dso-data-redirect.service.spec.ts => dso-redirect-data.service.spec.ts} (96%) rename src/app/core/data/{dso-data-redirect.service.ts => dso-redirect-data.service.ts} (97%) diff --git a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts index 012345e791..758287bcbc 100644 --- a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts +++ b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts @@ -1,12 +1,36 @@ import { LookupGuard } from './lookup-guard'; import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, UrlSegment } from '@angular/router'; import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; +import { hasValue } from '../shared/empty.util'; @NgModule({ imports: [ RouterModule.forChild([ - { path: ':idType/:id', canActivate: [LookupGuard], component: ObjectNotFoundComponent } + { + matcher: (url) => { + // The expected path is :idType/:id + const idType = url[0].path; + let id; + // Allow for handles that are delimited with a forward slash. + if (url.length === 3) { + id = url[1].path + '/' + url[2].path; + } else { + id = url[1].path; + } + if (hasValue(idType) && hasValue(id)) { + return { + consumed: url, + posParams: { + idType: new UrlSegment(idType, {}), + id: new UrlSegment(id, {}) + } + }; + } + return null; + }, + canActivate: [LookupGuard], + component: ObjectNotFoundComponent } ]) ], providers: [ diff --git a/src/app/+lookup-by-id/lookup-by-id.module.ts b/src/app/+lookup-by-id/lookup-by-id.module.ts index 4620f57824..1b070c1279 100644 --- a/src/app/+lookup-by-id/lookup-by-id.module.ts +++ b/src/app/+lookup-by-id/lookup-by-id.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { LookupRoutingModule } from './lookup-by-id-routing.module'; import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; -import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; +import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; @NgModule({ imports: [ @@ -15,7 +15,7 @@ import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; ObjectNotFoundComponent ], providers: [ - DsoDataRedirectService + DsoRedirectDataService ] }) export class LookupIdModule { diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index 7f38b6db7a..5cc101f2b0 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -1,5 +1,5 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; -import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; +import { DsoRedirectDataService } from '../core/data/dso-redirect-data.service'; import { Injectable } from '@angular/core'; import { IdentifierType } from '../core/index/index.reducer'; import { Observable } from 'rxjs'; @@ -14,7 +14,7 @@ interface LookupParams { @Injectable() export class LookupGuard implements CanActivate { - constructor(private dsoService: DsoDataRedirectService) { + constructor(private dsoService: DsoRedirectDataService) { } canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable { diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts similarity index 96% rename from src/app/core/data/dso-data-redirect.service.spec.ts rename to src/app/core/data/dso-redirect-data.service.spec.ts index f2c35727fa..3bc37b872d 100644 --- a/src/app/core/data/dso-data-redirect.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -9,13 +9,13 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { IdentifierType } from '../index/index.reducer'; -import { DsoDataRedirectService } from './dso-data-redirect.service'; +import { DsoRedirectDataService } from './dso-redirect-data.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; -describe('DsoDataRedirectService', () => { +describe('DsoRedirectDataService', () => { let scheduler: TestScheduler; - let service: DsoDataRedirectService; + let service: DsoRedirectDataService; let halService: HALEndpointService; let requestService: RequestService; let rdbService: RemoteDataBuildService; @@ -66,7 +66,7 @@ describe('DsoDataRedirectService', () => { a: remoteData }) }); - service = new DsoDataRedirectService( + service = new DsoRedirectDataService( requestService, rdbService, dataBuildService, diff --git a/src/app/core/data/dso-data-redirect.service.ts b/src/app/core/data/dso-redirect-data.service.ts similarity index 97% rename from src/app/core/data/dso-data-redirect.service.ts rename to src/app/core/data/dso-redirect-data.service.ts index 1cb3b20f2a..b8d4cf545d 100644 --- a/src/app/core/data/dso-data-redirect.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -20,7 +20,7 @@ import { getFinishedRemoteData } from '../shared/operators'; import { Router } from '@angular/router'; @Injectable() -export class DsoDataRedirectService extends DataService { +export class DsoRedirectDataService extends DataService { protected linkPath = 'pid'; protected forceBypassCache = false; From dfd1881f8922f71ad0bf01a48010f8943a20f0a8 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Fri, 18 Oct 2019 10:59:24 -0700 Subject: [PATCH 109/110] Added support for using the dso (uuid) endpoint. --- src/app/core/data/data.service.ts | 4 +-- .../core/data/dso-redirect-data.service.ts | 33 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 0f7ca74d15..7bfd1b6577 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -154,9 +154,7 @@ export abstract class DataService { map((endpoint: string) => this.getIDHref(endpoint, id))); } else if (identifierType === IdentifierType.HANDLE) { hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => { - return this.getIDHref(endpoint, encodeURIComponent(id)); - })); + map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id)))); } hrefObs.pipe( find((href: string) => hasValue(href))) diff --git a/src/app/core/data/dso-redirect-data.service.ts b/src/app/core/data/dso-redirect-data.service.ts index b8d4cf545d..b7acc371bf 100644 --- a/src/app/core/data/dso-redirect-data.service.ts +++ b/src/app/core/data/dso-redirect-data.service.ts @@ -14,7 +14,7 @@ import { IdentifierType } from '../index/index.reducer'; import { RemoteData } from './remote-data'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { Injectable } from '@angular/core'; -import { tap } from 'rxjs/operators'; +import { filter, tap } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { getFinishedRemoteData } from '../shared/operators'; import { Router } from '@angular/router'; @@ -22,8 +22,10 @@ import { Router } from '@angular/router'; @Injectable() export class DsoRedirectDataService extends DataService { + // Set the default link path to the identifier lookup endpoint. protected linkPath = 'pid'; protected forceBypassCache = false; + private uuidEndpoint = 'dso'; constructor( protected requestService: RequestService, @@ -43,28 +45,37 @@ export class DsoRedirectDataService extends DataService { return this.halService.getEndpoint(linkPath); } + setLinkPath(identifierType: IdentifierType) { + // The default 'pid' endpoint for identifiers does not support uuid lookups. + // For uuid lookups we need to change the linkPath. + if (identifierType === IdentifierType.UUID) { + this.linkPath = this.uuidEndpoint; + } + } + getIDHref(endpoint, resourceID): string { - return endpoint.replace(/\{\?id\}/,`?id=${resourceID}`); + // Supporting both identifier (pid) and uuid (dso) endpoints + return endpoint.replace(/\{\?id\}/, `?id=${resourceID}`) + .replace(/\{\?uuid\}/, `?uuid=${resourceID}`); } findById(id: string, identifierType = IdentifierType.UUID): Observable> { + this.setLinkPath(identifierType); return super.findById(id, identifierType).pipe( getFinishedRemoteData(), + filter((response) => response.hasSucceeded), tap((response) => { - if (response.hasSucceeded) { - const uuid = response.payload.uuid; - // Is there an existing method somewhere that converts dso type route? - const dsoType = this.getEndpointFromDSOType(response.payload.type); - if (hasValue(uuid) && hasValue(dsoType)) { - this.router.navigate([dsoType + '/' + uuid]); - } + const uuid = response.payload.uuid; + const newRoute = this.getEndpointFromDSOType(response.payload.type); + if (hasValue(uuid) && hasValue(newRoute)) { + this.router.navigate([newRoute + '/' + uuid]); } }) ); } - + // Is there an existing method somewhere else that converts dso type to route? getEndpointFromDSOType(dsoType: string): string { - // Are there other routes to consider? + // Are there other types to consider? if (dsoType.startsWith('item')) { return 'items' } else if (dsoType.startsWith('community')) { From 03c36ab23347c27956de1c6044834b270c668ea5 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Fri, 18 Oct 2019 11:18:21 -0700 Subject: [PATCH 110/110] Updated unit test. --- .../data/dso-redirect-data.service.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/app/core/data/dso-redirect-data.service.spec.ts b/src/app/core/data/dso-redirect-data.service.spec.ts index 3bc37b872d..84ca3918b8 100644 --- a/src/app/core/data/dso-redirect-data.service.spec.ts +++ b/src/app/core/data/dso-redirect-data.service.spec.ts @@ -84,12 +84,28 @@ describe('DsoRedirectDataService', () => { describe('findById', () => { it('should call HALEndpointService with the path to the pid endpoint', () => { setup(); - scheduler.schedule(() => service.findById(dsoUUID)); + scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); scheduler.flush(); expect(halService.getEndpoint).toHaveBeenCalledWith('pid'); }); + it('should call HALEndpointService with the path to the dso endpoint', () => { + setup(); + scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); + scheduler.flush(); + + expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); + }); + + it('should call HALEndpointService with the path to the dso endpoint when identifier type not specified', () => { + setup(); + scheduler.schedule(() => service.findById(dsoUUID)); + scheduler.flush(); + + expect(halService.getEndpoint).toHaveBeenCalledWith('dso'); + }); + it('should configure the proper FindByIDRequest for uuid', () => { setup(); scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID));