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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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/126] 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 ed959e492a27213fc97b11a986c5778b85addbd2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 4 Jul 2019 11:17:56 +0200 Subject: [PATCH 068/126] Show only authorized collections list during submission --- src/app/core/data/collection-data.service.ts | 17 +++++++++++++++++ ...submission-form-collection.component.spec.ts | 16 +++++++++++++++- .../submission-form-collection.component.ts | 11 +++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 993954a360..762838e9ae 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs/internal/Observable'; import { FindAllOptions } from './request.models'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; +import { SearchParam } from '../cache/models/search-param.model'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -40,6 +41,22 @@ export class CollectionDataService extends ComColDataService { super(); } + /** + * Get all collections whom user has authorization to submit to by community + * + * @return boolean + * true if the user has at least one collection to submit to + */ + getAuthorizedCollectionByCommunity(communityId): Observable>> { + const searchHref = 'findAuthorizedByCommunity'; + const options = new FindAllOptions(); + options.elementsPerPage = 1000; + options.searchParams = [new SearchParam('uuid', communityId)]; + + return this.searchBy(searchHref, options).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + /** * Find whether there is a collection whom user has authorization to submit to * diff --git a/src/app/submission/form/collection/submission-form-collection.component.spec.ts b/src/app/submission/form/collection/submission-form-collection.component.spec.ts index 679500a670..fc34094ce0 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.spec.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.spec.ts @@ -8,6 +8,7 @@ import { filter } from 'rxjs/operators'; import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; +import { cold } from 'jasmine-marbles'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub'; import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/mock-submission'; @@ -24,7 +25,7 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; import { Collection } from '../../../core/shared/collection.model'; import { createTestComponent } from '../../../shared/testing/utils'; -import { cold } from 'jasmine-marbles'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; const subcommunities = [Object.assign(new Community(), { name: 'SubCommunity 1', @@ -125,6 +126,12 @@ const mockCommunity2 = Object.assign(new Community(), { const mockCommunityList = observableOf(new RemoteData(true, true, true, undefined, new PaginatedList(new PageInfo(), [mockCommunity, mockCommunity2]))); +const mockCommunityCollectionList = observableOf(new RemoteData(true, true, true, + undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2]))); + +const mockCommunity2CollectionList = observableOf(new RemoteData(true, true, true, + undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2]))); + const mockCollectionList = [ { communities: [ @@ -193,6 +200,11 @@ describe('SubmissionFormCollectionComponent Component', () => { const communityDataService: any = jasmine.createSpyObj('communityDataService', { findAll: jasmine.createSpy('findAll') }); + + const collectionDataService: any = jasmine.createSpyObj('collectionDataService', { + getAuthorizedCollectionByCommunity: jasmine.createSpy('getAuthorizedCollectionByCommunity') + }); + const store: any = jasmine.createSpyObj('store', { dispatch: jasmine.createSpy('dispatch'), select: jasmine.createSpy('select') @@ -214,6 +226,7 @@ describe('SubmissionFormCollectionComponent Component', () => { TestComponent ], providers: [ + { provide: CollectionDataService, useValue: collectionDataService }, { provide: SubmissionJsonPatchOperationsService, useClass: SubmissionJsonPatchOperationsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: CommunityDataService, useValue: communityDataService }, @@ -284,6 +297,7 @@ describe('SubmissionFormCollectionComponent Component', () => { it('should init collection list properly', () => { communityDataService.findAll.and.returnValue(mockCommunityList); + collectionDataService.getAuthorizedCollectionByCommunity.and.returnValues(mockCommunityCollectionList, mockCommunity2CollectionList); comp.ngOnChanges({ currentCollectionId: new SimpleChange(null, collectionId, true) diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index b576834091..eb7459eaf4 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -35,6 +35,8 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { SubmissionService } from '../../submission.service'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { FindAllOptions } from '../../../core/data/request.models'; /** * An interface to represent a collection entry @@ -145,12 +147,14 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { * * @param {ChangeDetectorRef} cdr * @param {CommunityDataService} communityDataService + * @param {CollectionDataService} collectionDataService * @param {JsonPatchOperationsBuilder} operationsBuilder * @param {SubmissionJsonPatchOperationsService} operationsService * @param {SubmissionService} submissionService */ constructor(protected cdr: ChangeDetectorRef, private communityDataService: CommunityDataService, + private collectionDataService: CollectionDataService, private operationsBuilder: JsonPatchOperationsBuilder, private operationsService: SubmissionJsonPatchOperationsService, private submissionService: SubmissionService) { @@ -189,16 +193,19 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { if (hasValue(changes.currentCollectionId) && hasValue(changes.currentCollectionId.currentValue)) { this.selectedCollectionId = this.currentCollectionId; + const findOptions: FindAllOptions = { + elementsPerPage: 100 + }; // @TODO replace with search/top browse endpoint // @TODO implement community/subcommunity hierarchy - const communities$ = this.communityDataService.findAll().pipe( + const communities$ = this.communityDataService.findAll(findOptions).pipe( find((communities: RemoteData>) => isNotEmpty(communities.payload)), mergeMap((communities: RemoteData>) => communities.payload.page)); const listCollection$ = communities$.pipe( flatMap((communityData: Community) => { - return communityData.collections.pipe( + return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid).pipe( find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), mergeMap((collections: RemoteData>) => collections.payload.page), filter((collectionData: Collection) => isNotEmpty(collectionData)), From b665456b9d8bf30c8d559863316127b126225fd7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 4 Jul 2019 13:03:18 +0200 Subject: [PATCH 069/126] Fixed issues with relation group field --- src/app/shared/chips/models/chips-item.model.ts | 8 +++++++- .../ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts | 7 +++++++ .../relation-group/dynamic-relation-group.components.ts | 4 +++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts index 540f94166f..913232fa71 100644 --- a/src/app/shared/chips/models/chips-item.model.ts +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -2,6 +2,7 @@ import { isObject, uniqueId } from 'lodash'; import { hasValue, isNotEmpty } from '../../empty.util'; import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { ConfidenceType } from '../../../core/integration/models/confidence-type'; +import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; export interface ChipsItemIcon { metadata: string; @@ -62,7 +63,7 @@ export class ChipsItem { if (this._item.hasOwnProperty(icon.metadata) && (((typeof this._item[icon.metadata] === 'string') && hasValue(this._item[icon.metadata])) || (this._item[icon.metadata] as FormFieldMetadataValueObject).hasValue()) - && !(this._item[icon.metadata] as FormFieldMetadataValueObject).hasPlaceholder()) { + && !this.hasPlaceholder(this._item[icon.metadata])) { if ((icon.visibleWhenAuthorityEmpty || (this._item[icon.metadata] as FormFieldMetadataValueObject).confidence !== ConfidenceType.CF_UNSET) && isNotEmpty(icon.style)) { @@ -109,4 +110,9 @@ export class ChipsItem { this.display = value; } + + private hasPlaceholder(value: any) { + return (typeof value === 'string') ? (value === PLACEHOLDER_PARENT_METADATA) : + (value as FormFieldMetadataValueObject).hasPlaceholder() + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index fc618023f9..66bdf97dad 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -1,4 +1,7 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; + +import { Subject } from 'rxjs'; + import { isNotEmpty } from '../../../../empty.util'; import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; @@ -16,12 +19,16 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() separator: string; @serializable() hasLanguages = false; isCustomGroup = true; + valueUpdates: Subject; constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.separator = config.separator + ' '; + + this.valueUpdates = new Subject(); + this.valueUpdates.subscribe((value: string) => this.value = value); } get value() { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index fde8d4b7bf..1485993375 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -130,7 +130,9 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent ? null : this.selectedChipItem.item[model.name]; if (isNotNull(value)) { - model.valueUpdates.next(this.formBuilderService.isInputModel(model) ? value.value : value); + const nextValue = (this.formBuilderService.isInputModel(model) && (typeof value !== 'string')) ? + value.value : value; + model.valueUpdates.next(nextValue); } }); }); From fe1e4931c32fd6c00e2b120f4be257f24ebec45b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 20 May 2019 12:53:07 +0200 Subject: [PATCH 070/126] hide upload section on init when is not mandatory and there a no file uploaded in the submission --- .../core/submission/submission-response-parsing.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index 21135be463..5206227d4e 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -131,7 +131,10 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService // Iterate over all workspaceitem's sections Object.keys(item.sections) .forEach((sectionId) => { - if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) { + if (typeof item.sections[sectionId] === 'object' && (isNotEmpty(item.sections[sectionId]) && + // When Upload section is disabled, add to submission only if there are files + (!item.sections[sectionId].hasOwnProperty('files') || isNotEmpty((item.sections[sectionId] as any).files)))) { + const normalizedSectionData = Object.create({}); // Iterate over all sections property Object.keys(item.sections[sectionId]) From 4a1530eea5e31d327b4350299831b9a4d38bbc67 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 May 2019 13:15:10 +0200 Subject: [PATCH 071/126] fixed issue with submission footer z-index --- src/styles/_custom_variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index be03d719c5..b4de04ad9e 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -11,7 +11,7 @@ $drop-zone-area-inner-z-index: 1021; $login-logo-height:72px; $login-logo-width:72px; $submission-header-z-index: 1001; -$submission-footer-z-index: 1000; +$submission-footer-z-index: 999; $main-z-index: 0; $nav-z-index: 10; From 6856f95cb94ca7fdc911a79c9455317656b33389 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 5 Jul 2019 13:10:51 +0200 Subject: [PATCH 072/126] Fixed an issue when submission metadata auto save is triggered on a typeahead field --- .../dynamic-typeahead.component.html | 3 +- .../dynamic-typeahead.component.spec.ts | 47 +++++++++++++++++-- .../typeahead/dynamic-typeahead.component.ts | 30 ++++++++---- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html index 51e7667200..449481152d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html @@ -28,7 +28,8 @@ aria-hidden="true" [authorityValue]="currentValue" (whenClickOnConfidenceNotAccepted)="whenClickOnConfidenceNotAccepted($event)"> - { inputElement.value = 'test value'; inputElement.dispatchEvent(new Event('input')); - expect((typeaheadComp.model as any).value).toEqual(new FormFieldMetadataValueObject('test value')) + expect(typeaheadComp.inputValue).toEqual(new FormFieldMetadataValueObject('test value')) }); @@ -173,19 +173,56 @@ describe('DsDynamicTypeaheadComponent test suite', () => { }); - it('should emit blur Event onBlur', () => { + it('should emit blur Event onBlur when popup is closed', () => { spyOn(typeaheadComp.blur, 'emit'); + spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); typeaheadComp.onBlur(new Event('blur')); expect(typeaheadComp.blur.emit).toHaveBeenCalled(); }); - it('should emit change Event onBlur when AuthorityOptions.closed is false', () => { + it('should not emit blur Event onBlur when popup is opened', () => { + spyOn(typeaheadComp.blur, 'emit'); + spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(true); + const input = typeaheadFixture.debugElement.query(By.css('input')); + + input.nativeElement.blur(); + expect(typeaheadComp.blur.emit).not.toHaveBeenCalled(); + }); + + it('should emit change Event onBlur when AuthorityOptions.closed is false and inputValue is changed', () => { typeaheadComp.inputValue = 'test value'; typeaheadFixture.detectChanges(); spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.change, 'emit'); - typeaheadComp.onBlur(new Event('blur')); - // expect(typeaheadComp.change.emit).toHaveBeenCalled(); + spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); + typeaheadComp.onBlur(new Event('blur', )); + expect(typeaheadComp.change.emit).toHaveBeenCalled(); + expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + }); + + it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is not changed', () => { + typeaheadComp.inputValue = 'test value'; + typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + (typeaheadComp.model as any).value = 'test value'; + typeaheadFixture.detectChanges(); + spyOn(typeaheadComp.blur, 'emit'); + spyOn(typeaheadComp.change, 'emit'); + spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); + typeaheadComp.onBlur(new Event('blur', )); + expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); + expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + }); + + it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is null', () => { + typeaheadComp.inputValue = null; + typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + (typeaheadComp.model as any).value = 'test value'; + typeaheadFixture.detectChanges(); + spyOn(typeaheadComp.blur, 'emit'); + spyOn(typeaheadComp.change, 'emit'); + spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); + typeaheadComp.onBlur(new Event('blur', )); + expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled(); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts index ace6812858..136d1db1c2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { @@ -8,14 +8,13 @@ import { } from '@ng-dynamic-forms/core'; import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators'; import { Observable, of as observableOf, Subject } from 'rxjs'; -import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; +import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; -import { isEmpty, isNotEmpty } from '../../../../../empty.util'; +import { isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; - import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type'; @Component({ @@ -32,6 +31,8 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp @Output() change: EventEmitter = new EventEmitter(); @Output() focus: EventEmitter = new EventEmitter(); + @ViewChild('instance') instance: NgbTypeahead; + searching = false; searchOptions: IntegrationSearchOptions; searchFailed = false; @@ -105,16 +106,26 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp onInput(event) { if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { this.inputValue = new FormFieldMetadataValueObject(event.target.value); - this.model.valueUpdates.next(this.inputValue); } } onBlur(event: Event) { - if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) { - this.change.emit(this.inputValue); - this.inputValue = null; + if (!this.instance.isPopupOpen()) { + if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) { + if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { + this.model.valueUpdates.next(this.inputValue); + this.change.emit(this.inputValue); + } + this.inputValue = null; + } + this.blur.emit(event); + } else { + // prevent on blur propagation if typeahed suggestions are showed + event.preventDefault(); + event.stopImmediatePropagation(); + // set focus on input again, this is to avoid to lose changes when no suggestion is selected + (event.target as HTMLInputElement).focus(); } - this.blur.emit(event); } onChange(event: Event) { @@ -141,4 +152,5 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp this.click$.next(this.formatter(this.currentValue)); } } + } From 016afd84e466638213a4a6f5386367db6ba61f24 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 8 Jul 2019 10:40:41 +0200 Subject: [PATCH 073/126] Fixed edit link for workflow --- .../claimed-task/claimed-task-actions.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html index 4b9b93e7e3..3a8cb0cded 100644 --- a/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html +++ b/src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html @@ -2,7 +2,7 @@ {{'submission.workflow.tasks.claimed.edit' | translate}} From 7667cab772fd51ca5ebe8381cf144553bc7a9bd9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 May 2019 17:58:37 +0200 Subject: [PATCH 074/126] Added hint message to form fields --- .../ds-dynamic-form-ui/models/ds-dynamic-input.model.ts | 7 ++----- .../models/ds-dynamic-qualdrop.model.ts | 8 ++++++-- src/app/shared/form/builder/parsers/field-parser.ts | 2 ++ .../shared/form/builder/parsers/onebox-field-parser.ts | 4 ++++ src/app/shared/form/form.component.scss | 5 +++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 860c481820..4e4a944319 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -28,6 +28,7 @@ export class DsDynamicInputModel extends DynamicInputModel { constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); + this.hint = config.hint; this.readOnly = config.readOnly; this.value = config.value; this.language = config.language; @@ -57,11 +58,7 @@ export class DsDynamicInputModel extends DynamicInputModel { } get hasLanguages(): boolean { - if (this.languageCodes && this.languageCodes.length > 1) { - return true; - } else { - return false; - } + return this.languageCodes && this.languageCodes.length > 1; } get language(): string { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts index 6bd5a604a0..5d2cbc58b7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts @@ -1,5 +1,5 @@ -import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; +import { DynamicFormControlLayout, DynamicFormGroupModel, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { Subject } from 'rxjs'; import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model'; import { LanguageCode } from '../../models/form-field-language-value.model'; @@ -12,6 +12,7 @@ export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfi languageCodes?: LanguageCode[]; language?: string; readOnly: boolean; + hint?: string; } export class DynamicQualdropModel extends DynamicFormGroupModel { @@ -20,6 +21,7 @@ export class DynamicQualdropModel extends DynamicFormGroupModel { @serializable() languageUpdates: Subject; @serializable() hasLanguages = false; @serializable() readOnly: boolean; + @serializable() hint: string; isCustomGroup = true; constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) { @@ -33,6 +35,8 @@ export class DynamicQualdropModel extends DynamicFormGroupModel { this.languageUpdates.subscribe((lang: string) => { this.language = lang; }); + + this.hint = config.hint; } get value() { diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 28e3fb8fb5..dd37a45fba 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -190,6 +190,8 @@ export abstract class FieldParser { controlModel.placeholder = this.configData.label; + controlModel.hint = this.configData.hints; + if (this.configData.mandatory && setErrors) { this.markAsRequired(controlModel); } diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index d347f38eee..284656cc95 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -24,6 +24,7 @@ export class OneboxFieldParser extends FieldParser { const clsGroup = { element: { control: 'form-row', + hint: 'ds-form-qualdrop-hint' } }; @@ -54,8 +55,10 @@ export class OneboxFieldParser extends FieldParser { inputSelectGroup.id = newId.replace(/\./g, '_') + QUALDROP_GROUP_SUFFIX; inputSelectGroup.group = []; inputSelectGroup.legend = this.configData.label; + inputSelectGroup.hint = this.configData.hints; const selectModelConfig: DynamicSelectModelConfig = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label); + selectModelConfig.hint = null; this.setOptions(selectModelConfig); if (isNotEmpty(fieldValue)) { selectModelConfig.value = fieldValue.metadata; @@ -63,6 +66,7 @@ export class OneboxFieldParser extends FieldParser { inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, true); + inputModelConfig.hint = null; this.setValues(inputModelConfig, fieldValue); inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; diff --git a/src/app/shared/form/form.component.scss b/src/app/shared/form/form.component.scss index 1d5e034290..ed10941f09 100644 --- a/src/app/shared/form/form.component.scss +++ b/src/app/shared/form/form.component.scss @@ -44,3 +44,8 @@ .right-addon input { padding-right: $spacer * 2.25; } + +.ds-form-qualdrop-hint { + top: -$spacer; + position: relative; +} From cf73625830af5215b013b97e7366009e6329ffe4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 Jul 2019 19:12:31 +0200 Subject: [PATCH 075/126] Fixed comments --- .../objects/submission-objects.reducer.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 1a65783945..8c111dde67 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -361,7 +361,7 @@ const addError = (state: SubmissionObjectState, action: InertSectionErrorsAction * @param state * the current state * @param action - * an RemoveSectionErrorsAction + * a RemoveSectionErrorsAction * @return SubmissionObjectState * the new state, with the section's errors updated. */ @@ -416,7 +416,7 @@ function initSubmission(state: SubmissionObjectState, action: InitSubmissionForm * @param state * the current state * @param action - * an ResetSubmissionFormAction + * a ResetSubmissionFormAction * @return SubmissionObjectState * the new state, with the section removed. */ @@ -439,7 +439,7 @@ function resetSubmission(state: SubmissionObjectState, action: ResetSubmissionFo * @param state * the current state * @param action - * an CompleteInitSubmissionFormAction + * a CompleteInitSubmissionFormAction * @return SubmissionObjectState * the new state, with the section removed. */ @@ -461,7 +461,7 @@ function completeInit(state: SubmissionObjectState, action: CompleteInitSubmissi * @param state * the current state * @param action - * an SaveSubmissionFormAction | SaveSubmissionSectionFormAction + * a SaveSubmissionFormAction | SaveSubmissionSectionFormAction * | SaveForLaterSubmissionFormAction | SaveAndDepositSubmissionAction * @return SubmissionObjectState * the new state, with the flag set to true. @@ -491,7 +491,7 @@ function saveSubmission(state: SubmissionObjectState, * @param state * the current state * @param action - * an SaveSubmissionFormSuccessAction | SaveForLaterSubmissionFormSuccessAction + * a SaveSubmissionFormSuccessAction | SaveForLaterSubmissionFormSuccessAction * | SaveSubmissionSectionFormSuccessAction | SaveSubmissionFormErrorAction * | SaveForLaterSubmissionFormErrorAction | SaveSubmissionSectionFormErrorAction * @return SubmissionObjectState @@ -521,7 +521,7 @@ function completeSave(state: SubmissionObjectState, * @param state * the current state * @param action - * an DepositSubmissionAction + * a DepositSubmissionAction * @return SubmissionObjectState * the new state, with the deposit flag changed. */ @@ -544,7 +544,7 @@ function startDeposit(state: SubmissionObjectState, action: DepositSubmissionAct * @param state * the current state * @param action - * an DepositSubmissionSuccessAction or DepositSubmissionErrorAction + * a DepositSubmissionSuccessAction or a DepositSubmissionErrorAction * @return SubmissionObjectState * the new state, with the deposit flag changed. */ @@ -586,7 +586,7 @@ function changeCollection(state: SubmissionObjectState, action: ChangeSubmission * @param state * the current state * @param action - * an SetActiveSectionAction + * a SetActiveSectionAction * @return SubmissionObjectState * the new state, with the active section. */ @@ -676,7 +676,7 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa * @param state * the current state * @param action - * an DisableSectionAction + * a DisableSectionAction * @param enabled * enabled or disabled section. * @return SubmissionObjectState @@ -705,7 +705,7 @@ function changeSectionState(state: SubmissionObjectState, action: EnableSectionA * @param state * the current state * @param action - * an SectionStatusChangeAction + * a SectionStatusChangeAction * @return SubmissionObjectState * the new state, with the section new validity status. */ @@ -769,7 +769,7 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S * @param state * the current state * @param action - * a EditFileDataAction action + * an EditFileDataAction action * @return SubmissionObjectState * the new state, with the edited file. */ From ec105b35a9c937daf1690e30438001cbac04bf3a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 10:03:49 +0200 Subject: [PATCH 076/126] Disabled search button on lookup field on edit mode --- .../lookup/dynamic-lookup.component.spec.ts | 40 +++++++++++++++++++ .../models/lookup/dynamic-lookup.component.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index df2252163d..4a1d636adb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -237,6 +237,12 @@ describe('Dynamic Lookup component', () => { it('should init component properly', () => { expect(lookupComp.firstInputValue).toBe(''); + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = de[0].nativeElement; + const editBtnEl = de[1].nativeElement; + expect(searchBtnEl.disabled).toBe(true); + expect(editBtnEl.disabled).toBe(true); + expect(editBtnEl.textContent.trim()).toBe('form.edit'); }); it('should return search results', fakeAsync(() => { @@ -297,6 +303,7 @@ describe('Dynamic Lookup component', () => { expect(lookupComp.model.value).not.toBeDefined(); }); + }); describe('and init model value is not empty', () => { @@ -318,6 +325,19 @@ describe('Dynamic Lookup component', () => { it('should init component properly', () => { expect(lookupComp.firstInputValue).toBe('test'); }); + + it('should have search button disabled on edit mode', () => { + lookupComp.editMode = true; + lookupFixture.detectChanges(); + + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = de[0].nativeElement; + const saveBtnEl = de[1].nativeElement; + expect(searchBtnEl.disabled).toBe(true); + expect(saveBtnEl.disabled).toBe(false); + expect(saveBtnEl.textContent.trim()).toBe('form.save'); + + }); }); }); @@ -340,7 +360,14 @@ describe('Dynamic Lookup component', () => { }); it('should render two input element', () => { const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); + const deBtn = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = deBtn[0].nativeElement; + const editBtnEl = deBtn[1].nativeElement; + expect(de.length).toBe(2); + expect(searchBtnEl.disabled).toBe(true); + expect(editBtnEl.disabled).toBe(true); + expect(editBtnEl.textContent.trim()).toBe('form.edit'); }); }); @@ -418,6 +445,19 @@ describe('Dynamic Lookup component', () => { expect(lookupComp.firstInputValue).toBe('Name'); expect(lookupComp.secondInputValue).toBe('Lastname'); }); + + it('should have search button disabled on edit mode', () => { + lookupComp.editMode = true; + lookupFixture.detectChanges(); + + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = de[0].nativeElement; + const saveBtnEl = de[1].nativeElement; + expect(searchBtnEl.disabled).toBe(true); + expect(saveBtnEl.disabled).toBe(false); + expect(saveBtnEl.textContent.trim()).toBe('form.save'); + + }); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index cba352484d..597f39b271 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -159,7 +159,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem } public isSearchDisabled() { - return isEmpty(this.firstInputValue); + return isEmpty(this.firstInputValue) || this.editMode; } public onBlurEvent(event: Event) { From 93b415a6bed431b220e2cd4f716beced838127d8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 10:05:14 +0200 Subject: [PATCH 077/126] Checked there is no pending save when a new save operation is dispatched --- src/app/submission/submission.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 82185a8eae..f35536d560 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -184,7 +184,11 @@ export class SubmissionService { * The submission id */ dispatchSave(submissionId) { - this.store.dispatch(new SaveSubmissionFormAction(submissionId)); + this.getSubmissionSaveProcessingStatus(submissionId).pipe( + find((isPending: boolean) => !isPending) + ).subscribe(() => { + this.store.dispatch(new SaveSubmissionFormAction(submissionId)); + }) } /** From a2180c19ac2dc0d425d934d46252f911f206041a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 11:58:21 +0200 Subject: [PATCH 078/126] during submission retrieve surrent collection name by current collection id --- .../submission-form-collection.component.html | 3 +-- ...bmission-form-collection.component.spec.ts | 10 +++++++--- .../submission-form-collection.component.ts | 19 ++++++------------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index 37ada35155..2cd1e928f7 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -12,8 +12,7 @@ (click)="onClose()" [disabled]="(disabled$ | async)" ngbDropdownToggle> - - {{ selectedCollectionName$ | async }} + {{ selectedCollectionName$ | async }}
{ }); const collectionDataService: any = jasmine.createSpyObj('collectionDataService', { + findById: jasmine.createSpy('findById'), getAuthorizedCollectionByCommunity: jasmine.createSpy('getAuthorizedCollectionByCommunity') }); @@ -297,6 +301,7 @@ describe('SubmissionFormCollectionComponent Component', () => { it('should init collection list properly', () => { communityDataService.findAll.and.returnValue(mockCommunityList); + collectionDataService.findById.and.returnValue(mockCommunity1Collection1Rd); collectionDataService.getAuthorizedCollectionByCommunity.and.returnValues(mockCommunityCollectionList, mockCommunity2CollectionList); comp.ngOnChanges({ @@ -308,9 +313,8 @@ describe('SubmissionFormCollectionComponent Component', () => { b: mockCollectionList })); - expect(comp.selectedCollectionName$).toBeObservable(cold('(ab|)', { - a: '', - b: 'Community 1-Collection 1' + expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', { + a: 'Community 1-Collection 1' })); }); diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index eb7459eaf4..9cf01b173c 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -193,6 +193,12 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { if (hasValue(changes.currentCollectionId) && hasValue(changes.currentCollectionId.currentValue)) { this.selectedCollectionId = this.currentCollectionId; + + this.selectedCollectionName$ = this.collectionDataService.findById(this.currentCollectionId).pipe( + find((collectionRD: RemoteData) => isNotEmpty(collectionRD.payload)), + map((collectionRD: RemoteData) => collectionRD.payload.name) + ); + const findOptions: FindAllOptions = { elementsPerPage: 100 }; @@ -219,19 +225,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { startWith([]) ); - this.selectedCollectionName$ = communities$.pipe( - flatMap((communityData: Community) => { - return communityData.collections.pipe( - find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), - mergeMap((collections: RemoteData>) => collections.payload.page), - filter((collectionData: Collection) => isNotEmpty(collectionData)), - filter((collectionData: Collection) => collectionData.id === this.selectedCollectionId), - map((collectionData: Collection) => collectionData.name) - ); - }), - startWith('') - ); - const searchTerm$ = this.searchField.valueChanges.pipe( debounceTime(200), distinctUntilChanged(), From 49c341b05aa1cff73aeba34a2fb403394e9e4341 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 12:54:23 +0200 Subject: [PATCH 079/126] Fixed concat field hint message --- src/app/shared/form/builder/parsers/concat-field-parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 135dda5de3..6323905555 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -47,6 +47,7 @@ export class ConcatFieldParser extends FieldParser { const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, label, false, false); const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, label, true, false); + input2ModelConfig.hint = ' '; if (this.configData.mandatory) { input1ModelConfig.required = true; From 0ab2ca18809423b5a420399a916f513cb3c1e2e4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 12:59:16 +0200 Subject: [PATCH 080/126] added variable representing collection change processing status --- .../submission-form-collection.component.html | 5 +++-- .../collection/submission-form-collection.component.ts | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index 2cd1e928f7..6f4a8a864c 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -10,9 +10,10 @@ class="btn btn-outline-primary" (blur)="onClose()" (click)="onClose()" - [disabled]="(disabled$ | async)" + [disabled]="(disabled$ | async) || (processingChange$ | async)" ngbDropdownToggle> - {{ selectedCollectionName$ | async }} + + {{ selectedCollectionName$ | async }}
(true); + /** + * A boolean representing if a collection change operation is processing + * @type {BehaviorSubject} + */ + public processingChange$ = new BehaviorSubject(false); + /** * The search form control * @type {FormControl} @@ -265,7 +271,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { */ onSelect(event) { this.searchField.reset(); - this.disabled$.next(true); + this.processingChange$.next(true); this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true); this.subs.push(this.operationsService.jsonPatchByResourceID( this.submissionService.getSubmissionObjectLinkName(), @@ -277,7 +283,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { this.selectedCollectionName$ = observableOf(event.collection.name); this.collectionChange.emit(submissionObject[0]); this.submissionService.changeSubmissionCollection(this.submissionId, event.collection.id); - this.disabled$.next(false); + this.processingChange$.next(false); this.cdr.detectChanges(); }) ); From 82426700fdaf3c4c9d09137c0a8b3635b8334db5 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 13:00:26 +0200 Subject: [PATCH 081/126] Added getAuthorizedCollection method to CollectionDataService --- src/app/core/data/collection-data.service.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 762838e9ae..643eb55404 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -41,11 +41,26 @@ export class CollectionDataService extends ComColDataService { super(); } + /** + * Get all collections whom user has authorization to submit + * + * @return Observable>> + * collection list + */ + getAuthorizedCollection(): Observable>> { + const searchHref = 'findAuthorized'; + const options = new FindAllOptions(); + options.elementsPerPage = 1000; + + return this.searchBy(searchHref, options).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + /** * Get all collections whom user has authorization to submit to by community * - * @return boolean - * true if the user has at least one collection to submit to + * @return Observable>> + * collection list */ getAuthorizedCollectionByCommunity(communityId): Observable>> { const searchHref = 'findAuthorizedByCommunity'; From 2ff1b88bac89f40c6cb6eeee25944490a39e5929 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 19:14:57 +0200 Subject: [PATCH 082/126] Fixed issue with collection change --- .../submission/sections/upload/section-upload.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 826385af45..9dbd1079f4 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -155,14 +155,14 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { filter((submissionObject: SubmissionObjectEntry) => isUndefined(this.collectionId) || this.collectionId !== submissionObject.collection), tap((submissionObject: SubmissionObjectEntry) => this.collectionId = submissionObject.collection), flatMap((submissionObject: SubmissionObjectEntry) => this.collectionDataService.findById(submissionObject.collection)), - find((rd: RemoteData) => isNotUndefined((rd.payload))), + filter((rd: RemoteData) => isNotUndefined((rd.payload))), tap((collectionRemoteData: RemoteData) => this.collectionName = collectionRemoteData.payload.name), flatMap((collectionRemoteData: RemoteData) => { return this.resourcePolicyService.findByHref( (collectionRemoteData.payload as any)._links.defaultAccessConditions ); }), - find((defaultAccessConditionsRemoteData: RemoteData) => + filter((defaultAccessConditionsRemoteData: RemoteData) => defaultAccessConditionsRemoteData.hasSucceeded), tap((defaultAccessConditionsRemoteData: RemoteData) => { if (isNotEmpty(defaultAccessConditionsRemoteData.payload)) { @@ -171,7 +171,6 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { } }), flatMap(() => config$), - take(1), flatMap((config: SubmissionUploadsModel) => { this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : []; From 50024c8c559216053fc8323c91a4a2b8a816ae4e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 19:16:08 +0200 Subject: [PATCH 083/126] During submission retrieve collection list only at first load --- .../submission-form-collection.component.ts | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index 5a0e86103c..1fbccb6958 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -209,43 +209,46 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { elementsPerPage: 100 }; - // @TODO replace with search/top browse endpoint - // @TODO implement community/subcommunity hierarchy - const communities$ = this.communityDataService.findAll(findOptions).pipe( - find((communities: RemoteData>) => isNotEmpty(communities.payload)), - mergeMap((communities: RemoteData>) => communities.payload.page)); + // Retrieve collection list only when is the first change + if (changes.currentCollectionId.isFirstChange()) { + // @TODO replace with search/top browse endpoint + // @TODO implement community/subcommunity hierarchy + const communities$ = this.communityDataService.findAll(findOptions).pipe( + find((communities: RemoteData>) => isNotEmpty(communities.payload)), + mergeMap((communities: RemoteData>) => communities.payload.page)); - const listCollection$ = communities$.pipe( - flatMap((communityData: Community) => { - return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid).pipe( - find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), - mergeMap((collections: RemoteData>) => collections.payload.page), - filter((collectionData: Collection) => isNotEmpty(collectionData)), - map((collectionData: Collection) => ({ - communities: [{ id: communityData.id, name: communityData.name }], - collection: { id: collectionData.id, name: collectionData.name } - })) - ); - }), - reduce((acc: any, value: any) => [...acc, ...value], []), - startWith([]) - ); + const listCollection$ = communities$.pipe( + flatMap((communityData: Community) => { + return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid).pipe( + find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), + mergeMap((collections: RemoteData>) => collections.payload.page), + filter((collectionData: Collection) => isNotEmpty(collectionData)), + map((collectionData: Collection) => ({ + communities: [{ id: communityData.id, name: communityData.name }], + collection: { id: collectionData.id, name: collectionData.name } + })) + ); + }), + reduce((acc: any, value: any) => [...acc, ...value], []), + startWith([]) + ); - const searchTerm$ = this.searchField.valueChanges.pipe( - debounceTime(200), - distinctUntilChanged(), - startWith('') - ); + const searchTerm$ = this.searchField.valueChanges.pipe( + debounceTime(200), + distinctUntilChanged(), + startWith('') + ); - this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe( - map(([searchTerm, listCollection]) => { - this.disabled$.next(isEmpty(listCollection)); - if (isEmpty(searchTerm)) { - return listCollection; - } else { - return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); - } - })); + this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe( + map(([searchTerm, listCollection]) => { + this.disabled$.next(isEmpty(listCollection)); + if (isEmpty(searchTerm)) { + return listCollection; + } else { + return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); + } + })); + } } } From 3fe67b8da1a01d0688d07f1b7b53ab41b7312c38 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jul 2019 19:18:11 +0200 Subject: [PATCH 084/126] changed elementsPerPage on CollectionDataService --- src/app/core/data/collection-data.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 643eb55404..626ddf233d 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -42,7 +42,7 @@ export class CollectionDataService extends ComColDataService { } /** - * Get all collections whom user has authorization to submit + * Get all collections the user is authorized to submit to * * @return Observable>> * collection list @@ -50,14 +50,14 @@ export class CollectionDataService extends ComColDataService { getAuthorizedCollection(): Observable>> { const searchHref = 'findAuthorized'; const options = new FindAllOptions(); - options.elementsPerPage = 1000; + options.elementsPerPage = 100; return this.searchBy(searchHref, options).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } /** - * Get all collections whom user has authorization to submit to by community + * Get all collections the user is authorized to submit to, by community * * @return Observable>> * collection list @@ -65,7 +65,7 @@ export class CollectionDataService extends ComColDataService { getAuthorizedCollectionByCommunity(communityId): Observable>> { const searchHref = 'findAuthorizedByCommunity'; const options = new FindAllOptions(); - options.elementsPerPage = 1000; + options.elementsPerPage = 100; options.searchParams = [new SearchParam('uuid', communityId)]; return this.searchBy(searchHref, options).pipe( From 45ab326355d48d7b1cc1e204920585f309b3b198 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 26 Jul 2019 15:29:00 +0200 Subject: [PATCH 085/126] Added FindAllOptions param to getAuthorizedCollection and getAuthorizedCollectionByCommunity --- src/app/core/data/collection-data.service.ts | 11 +++++------ .../submission-form-collection.component.ts | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 626ddf233d..04e483604c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -44,13 +44,12 @@ export class CollectionDataService extends ComColDataService { /** * Get all collections the user is authorized to submit to * + * @param options The [[FindAllOptions]] object * @return Observable>> * collection list */ - getAuthorizedCollection(): Observable>> { + getAuthorizedCollection(options: FindAllOptions = {}): Observable>> { const searchHref = 'findAuthorized'; - const options = new FindAllOptions(); - options.elementsPerPage = 100; return this.searchBy(searchHref, options).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); @@ -59,13 +58,13 @@ export class CollectionDataService extends ComColDataService { /** * Get all collections the user is authorized to submit to, by community * + * @param communityId The community id + * @param options The [[FindAllOptions]] object * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId): Observable>> { + getAuthorizedCollectionByCommunity(communityId: string, options: FindAllOptions = {}): Observable>> { const searchHref = 'findAuthorizedByCommunity'; - const options = new FindAllOptions(); - options.elementsPerPage = 100; options.searchParams = [new SearchParam('uuid', communityId)]; return this.searchBy(searchHref, options).pipe( diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index 1fbccb6958..79d2f2a7bc 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -206,7 +206,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { ); const findOptions: FindAllOptions = { - elementsPerPage: 100 + elementsPerPage: 1000 }; // Retrieve collection list only when is the first change @@ -219,7 +219,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { const listCollection$ = communities$.pipe( flatMap((communityData: Community) => { - return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid).pipe( + return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), mergeMap((collections: RemoteData>) => collections.payload.page), filter((collectionData: Collection) => isNotEmpty(collectionData)), From 861f8ddb6c770f0a31caf53a0e9aa11b64c7605e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 26 Jul 2019 19:41:28 +0200 Subject: [PATCH 086/126] Show hint message only when there is an error message --- .../ds-dynamic-form-control-container.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index cead04f797..5692c27d20 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -14,7 +14,8 @@ - +
{{ message | translate:model.validators }} From 509fd0d802d8ff7070e6ab3a4569c747c32c8bb0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 26 Jul 2019 19:43:15 +0200 Subject: [PATCH 087/126] Fixed an issue while editing repeatable fields --- .../sections/form/section-form.component.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index ef817a7568..2269ccd5f1 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -64,6 +64,12 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ public isLoading = true; + /** + * A map representing all field on their way to be removed + * @type {Map} + */ + protected fieldsOnTheirWayToBeRemoved: Map = new Map(); + /** * The form config * @type {SubmissionFormsModel} @@ -295,6 +301,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { }), distinctUntilChanged()) .subscribe((sectionState: SubmissionSectionObject) => { + this.fieldsOnTheirWayToBeRemoved = new Map(); this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errors); }) ) @@ -348,11 +355,24 @@ export class SubmissionSectionformComponent extends SectionModelComponent { * the [[DynamicFormControlEvent]] emitted */ onRemove(event: DynamicFormControlEvent): void { + const fieldId = this.formBuilderService.getId(event.model); + const fieldIndex = this.formOperationsService.getArrayIndexFromEvent(event); + + // Keep track that this field will be removed + if (this.fieldsOnTheirWayToBeRemoved.has(fieldId)) { + const indexes = this.fieldsOnTheirWayToBeRemoved.get(fieldId); + indexes.push(fieldIndex); + this.fieldsOnTheirWayToBeRemoved.set(fieldId, indexes); + } else { + this.fieldsOnTheirWayToBeRemoved.set(fieldId, [fieldIndex]); + } + this.formOperationsService.dispatchOperationsFromEvent( this.pathCombiner, event, this.previousValue, - this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); + this.hasStoredValue(fieldId, fieldIndex)); + } /** @@ -365,9 +385,23 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ hasStoredValue(fieldId, index): boolean { if (isNotEmpty(this.sectionData.data)) { - return this.sectionData.data.hasOwnProperty(fieldId) && isNotEmpty(this.sectionData.data[fieldId][index]); + return this.sectionData.data.hasOwnProperty(fieldId) && + isNotEmpty(this.sectionData.data[fieldId][index]) && + !this.isFieldToRemove(fieldId, index); } else { return false; } } + + /** + * Check if the specified field is on the way to be removed + * + * @param fieldId + * the section data retrieved from the serverù + * @param index + * the section data retrieved from the server + */ + isFieldToRemove(fieldId, index) { + return this.fieldsOnTheirWayToBeRemoved.has(fieldId) && this.fieldsOnTheirWayToBeRemoved.get(fieldId).includes(index); + } } From 26e25069ad46c70ed5e8383f198c255c59886de1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 12 Aug 2019 16:41:56 +0200 Subject: [PATCH 088/126] 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 089/126] 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 090/126] 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, 20 Aug 2019 11:14:15 +0200 Subject: [PATCH 091/126] Fixed bitstream download in mydspace page --- .../item-detail-preview.component.html | 4 ++-- .../item-detail-preview.component.spec.ts | 14 +++++++++++ .../item-detail-preview.component.ts | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html index 04e128c49a..ab2c24c435 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html @@ -15,11 +15,11 @@ {{('mydspace.results.no-files' | translate)}} diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts index d0af614a6c..182c5eca39 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts @@ -12,6 +12,16 @@ import { MockTranslateLoader } from '../../../mocks/mock-translate-loader'; import { ItemDetailPreviewFieldComponent } from './item-detail-preview-field/item-detail-preview-field.component'; import { FileSizePipe } from '../../../utils/file-size-pipe'; import { VarDirective } from '../../../utils/var.directive'; +import { FileService } from '../../../../core/shared/file.service'; +import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; +import { HALEndpointServiceStub } from '../../../testing/hal-endpoint-service-stub'; + +function getMockFileService(): FileService { + return jasmine.createSpyObj('FileService', { + downloadFile: jasmine.createSpy('downloadFile'), + getFileNameFromResponseContentDisposition: jasmine.createSpy('getFileNameFromResponseContentDisposition') + }); +} let component: ItemDetailPreviewComponent; let fixture: ComponentFixture; @@ -59,6 +69,10 @@ describe('ItemDetailPreviewComponent', () => { }), ], declarations: [ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, TruncatePipe, FileSizePipe, VarDirective], + providers: [ + { provide: FileService, useValue: getMockFileService() }, + { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') } + ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemDetailPreviewComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts index d26bfc4589..fa15c71168 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts @@ -1,12 +1,15 @@ import { Component, Input } from '@angular/core'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { Item } from '../../../../core/shared/item.model'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { fadeInOut } from '../../../animations/fade'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { MyDSpaceResult } from '../../../../+my-dspace-page/my-dspace-result.model'; +import { FileService } from '../../../../core/shared/file.service'; +import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; /** * This component show metadata for the given item object in the detail view. @@ -54,6 +57,16 @@ export class ItemDetailPreviewComponent { */ public thumbnail$: Observable; + /** + * Initialize instance variables + * + * @param {FileService} fileService + * @param {HALEndpointService} halService + */ + constructor(private fileService: FileService, + private halService: HALEndpointService) { + } + /** * Initialize all instance variables */ @@ -62,4 +75,15 @@ export class ItemDetailPreviewComponent { this.bitstreams$ = this.item.getFiles(); } + /** + * Perform bitstream download + */ + public downloadBitstreamFile(uuid: string) { + this.halService.getEndpoint('bitstreams').pipe( + first()) + .subscribe((url) => { + const fileUrl = `${url}/${uuid}/content`; + this.fileService.downloadFile(fileUrl); + }); + } } From 3651b16b2c9b1d1879a9503477ab33e0cf231de7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 21 Aug 2019 09:52:38 +0200 Subject: [PATCH 092/126] Fixed issue with form value init within relation fields --- .../dynamic-relation-group.components.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 1485993375..62b6b4effa 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -129,11 +129,11 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent || this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA) ? null : this.selectedChipItem.item[model.name]; - if (isNotNull(value)) { - const nextValue = (this.formBuilderService.isInputModel(model) && (typeof value !== 'string')) ? - value.value : value; - model.valueUpdates.next(nextValue); - } + + const nextValue = (this.formBuilderService.isInputModel(model) && isNotNull(value) && (typeof value !== 'string')) ? + value.value : value; + model.valueUpdates.next(nextValue); + }); }); @@ -231,7 +231,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent flatMap((valueModel) => { const returnList: Array> = []; valueModel.forEach((valueObj) => { - const returnObj = Object.keys(valueObj).map((fieldName) => { + const returnObj = Object.keys(valueObj).map((fieldName) => { let return$: Observable; if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) { const fieldId = fieldName.replace(/\./g, '_'); @@ -255,7 +255,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent } else { return$ = observableOf(valueObj[fieldName]); } - return return$.pipe(map((entry) => ({[fieldName]: entry}))); + return return$.pipe(map((entry) => ({ [fieldName]: entry }))); }); returnList.push(combineLatest(returnObj)); From 5bcb361c6e36f886698df53562ae502ecbe78aac Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 21 Aug 2019 09:54:16 +0200 Subject: [PATCH 093/126] Fixed issue with model value update within scrollable fields --- .../dynamic-scrollable-dropdown.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index e51fc78ec3..5eda1372eb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro import { FormGroup } from '@angular/forms'; import { Observable, of as observableOf } from 'rxjs'; -import { catchError, first, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, first, tap } from 'rxjs/operators'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlComponent, @@ -71,7 +71,13 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp } this.pageInfo = object.pageInfo; this.cdr.detectChanges(); - }) + }); + + this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged()) + .subscribe((value) => { + this.setCurrentValue(value); + }); + } inputFormatter = (x: AuthorityValue): string => x.display || x.value; From c1b241dc299392842f6c5b4edae415867f5b6cd2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 22 Aug 2019 11:29:31 +0200 Subject: [PATCH 094/126] Fixed issue with lookup field which updated model on input event --- .../lookup/dynamic-lookup.component.html | 10 ++++---- .../lookup/dynamic-lookup.component.spec.ts | 4 ++-- .../models/lookup/dynamic-lookup.component.ts | 23 +++++++++++-------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index cb2d1fe217..3cfb5980c6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -20,11 +20,10 @@ [disabled]="isInputDisabled()" [placeholder]="model.placeholder | translate" [readonly]="model.readOnly" - (change)="$event.preventDefault()" + (change)="onChange($event)" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" - (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();" - (input)="onInput($event)"> + (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();">
@@ -40,11 +39,10 @@ [disabled]="firstInputValue.length === 0 || isInputDisabled()" [placeholder]="model.secondPlaceholder | translate" [readonly]="model.readOnly" - (change)="$event.preventDefault()" + (change)="onChange($event)" (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" - (click)="$event.stopPropagation(); sdRef.close();" - (input)="onInput($event)"> + (click)="$event.stopPropagation(); sdRef.close();">
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 108/126] 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 109/126] 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 110/126] 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 111/126] 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 112/126] 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 113/126] 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 114/126] 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 115/126] 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 116/126] 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 117/126] 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 118/126] 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 119/126] 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 120/126] 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 121/126] 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 122/126] 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 123/126] 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 124/126] 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 125/126] 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 126/126] 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;