From ec96962a87a5ff3d530d4aa435b50ca4aa3d325f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Oct 2018 17:50:52 +0200 Subject: [PATCH 001/149] 55946: Start of Edit Item Page Conflicts: resources/i18n/en.json --- resources/i18n/en.json | 61 +++++++++++++++++++ .../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, 181 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 b6a23068d7..deebe09760 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -43,6 +43,67 @@ "simple": "Simple item page", "full": "Full item page" } + }, + "select": { + "table": { + "collection": "Collection", + "author": "Author", + "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 05a4918ef0a15ba27c5a80c87767b88f7ed3352f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 11:51:52 +0200 Subject: [PATCH 002/149] 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 deebe09760..5cd54869d2 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -85,7 +85,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 fb0e1d81e4f461c8eac754831877d2494cb74ac7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 14:30:40 +0200 Subject: [PATCH 003/149] 55946: Edit item page cleanup --- .../edit-item-page/edit-item-page.module.ts | 4 +--- .../edit-item-page.routing.module.ts | 8 -------- .../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.ts | 8 +++++++- 7 files changed, 8 insertions(+), 38 deletions(-) delete mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html delete mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.scss delete mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts delete mode 100644 src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts 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..e7016eb05d 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 @@ -3,7 +3,6 @@ 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({ @@ -14,8 +13,7 @@ import { ItemStatusComponent } from './item-status/item-status.component'; ], declarations: [ EditItemPageComponent, - ItemStatusComponent, - ItemCollectionMapperComponent + ItemStatusComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index f2209cddcc..46e8dab609 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 @@ -2,7 +2,6 @@ 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: [ @@ -13,13 +12,6 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col resolve: { item: ItemPageResolver } - }, - { - path: 'map', - 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 deleted file mode 100644 index 3fb829fe8b..0000000000 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
-
-

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 deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index 592e3bd26c..0000000000 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -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.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 715614c1d9..8d68a9e961 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 @@ -53,8 +53,14 @@ export class ItemStatusComponent implements OnInit { }); this.statusDataKeys = Object.keys(this.statusData); + /* + The key is used to build messages + i18n example: 'item.edit.tabs.status.buttons..label' + The value is supposed to be a href for the button + */ this.actions = Object.assign({ - mappedCollections: this.getCurrentUrl() + '/map' + // TODO: Create mapping component on item level + mappedCollections: this.getCurrentUrl() + '/' }); this.actionsKeys = Object.keys(this.actions); } From 7d9afeefea78ee1cbe60a2ba74a687cf6014658f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 9 Oct 2018 15:32:55 +0200 Subject: [PATCH 004/149] 55946: Removed unnecessary files and created tests --- .../edit-item-page.component.scss | 0 .../edit-item-page.component.spec.ts | 0 .../edit-item-page.component.ts | 1 - .../item-status/item-status.component.html | 12 ++-- .../item-status/item-status.component.scss | 0 .../item-status/item-status.component.spec.ts | 67 +++++++++++++++++++ .../item-status/item-status.component.ts | 1 - 7 files changed, 73 insertions(+), 8 deletions(-) delete mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.scss delete mode 100644 src/app/+item-page/edit-item-page/edit-item-page.component.spec.ts delete mode 100644 src/app/+item-page/edit-item-page/item-status/item-status.component.scss 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 deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index e69de29bb2..0000000000 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 7702fc94e8..8bcf53f140 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 @@ -7,7 +7,6 @@ import { Item } from '../../core/shared/item.model'; @Component({ selector: 'ds-edit-item-page', - styleUrls: ['./edit-item-page.component.scss'], templateUrl: './edit-item-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ 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 index 0a93e7659d..78ab9174eb 100644 --- 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 @@ -1,27 +1,27 @@

{{'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}}
-
+
{{'item.edit.tabs.status.buttons.' + actionKey + '.button' | 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 deleted file mode 100644 index e69de29bb2..0000000000 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 index e69de29bb2..2df4b977cb 100644 --- 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 @@ -0,0 +1,67 @@ +import { ItemStatusComponent } from './item-status.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonModule } from '@angular/common'; +import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub'; +import { HostWindowService } from '../../../shared/host-window.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Router } from '@angular/router'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { Item } from '../../../core/shared/item.model'; +import { By } from '@angular/platform-browser'; + +describe('ItemStatusComponent', () => { + let comp: ItemStatusComponent; + let fixture: ComponentFixture; + + const mockItem = Object.assign(new Item(), { + id: 'fake-id', + handle: 'fake/handle', + lastModified: '2018' + }); + + const itemPageUrl = `fake-url/${mockItem.id}`; + const routerStub = Object.assign(new RouterStub(), { + url: `${itemPageUrl}/edit` + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [ItemStatusComponent], + providers: [ + { provide: Router, useValue: routerStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemStatusComponent); + comp = fixture.componentInstance; + comp.item = mockItem; + fixture.detectChanges(); + }); + + it('should display the item\'s internal id', () => { + const statusId: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-id')).nativeElement; + expect(statusId.textContent).toContain(mockItem.id); + }); + + it('should display the item\'s handle', () => { + const statusHandle: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-handle')).nativeElement; + expect(statusHandle.textContent).toContain(mockItem.handle); + }); + + it('should display the item\'s last modified date', () => { + const statusLastModified: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-lastModified')).nativeElement; + expect(statusLastModified.textContent).toContain(mockItem.lastModified); + }); + + it('should display the item\'s page url', () => { + const statusItemPage: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-itemPage')).nativeElement; + expect(statusItemPage.textContent).toContain(itemPageUrl); + }); + +}); 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 8d68a9e961..bc9dda61ec 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 @@ -5,7 +5,6 @@ import { Router } from '@angular/router'; @Component({ selector: 'ds-item-status', - styleUrls: ['./item-status.component.scss'], templateUrl: './item-status.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ From 8570ff25784f2ccf93913f6e7a4954621de50e8c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 25 Oct 2018 18:13:49 +0200 Subject: [PATCH 005/149] 55990: Item move component --- .../item-move/item-move.component.html | 51 +++++++++++++++++++ .../item-move/item-move.component.ts | 0 .../item-operation.component.html | 10 ++++ .../item-operation.component.ts | 0 .../item-operation/itemOperation.model.ts | 0 5 files changed, 61 insertions(+) create mode 100644 src/app/+item-page/edit-item-page/item-move/item-move.component.html create mode 100644 src/app/+item-page/edit-item-page/item-move/item-move.component.ts create mode 100644 src/app/+item-page/edit-item-page/item-operation/item-operation.component.html create mode 100644 src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts create mode 100644 src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html new file mode 100644 index 0000000000..a060aa0fed --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -0,0 +1,51 @@ +import {Component, OnInit} from '@angular/core'; +import {Collection} from '../../../core/shared/collection.model'; +import {RemoteData} from '../../../core/data/remote-data'; +import {Item} from '../../../core/shared/item.model'; +import {getSucceededRemoteData} from '../../../core/shared/operators'; +import {Observable} from 'rxjs'; +import {PaginatedList} from '../../../core/data/paginated-list'; +import {TranslateService} from '@ngx-translate/core'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {SearchService} from '../../../+search-page/search-service/search.service'; +import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {CollectionDataService} from '../../../core/data/collection-data.service'; + +@Component({ + selector: 'ds-item-move', + templateUrl: './item-move.component.html' +}) +export class ItemMoveComponent implements OnInit { + inheritPolicies: boolean; + + itemRD$: Observable>; + + /** + * List of collections to show under the "Browse" tab + * Collections that are mapped to the item + */ + itemCollectionsRD$: Observable>>; + + constructor(private route: ActivatedRoute, + private router: Router, + private searchConfigService: SearchConfigurationService, + private searchService: SearchService, + private notificationsService: NotificationsService, + private collectionDataService: CollectionDataService, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; + this.loadCollectionLists(); + } + + /** + * Load all available collections to move the item to. + * TODO: When the API support it, only fetch collections where user has ADD rights to. + */ + loadCollectionLists() { + this.itemCollectionsRD$ = this.collectionDataService.findAll(); + } +} diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html new file mode 100644 index 0000000000..59b625d8c0 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ds-item-operation', + templateUrl: './ds-item-operation.html' +}) + +export class ItemOperationComponent { + +} diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts new file mode 100644 index 0000000000..e69de29bb2 From d26bba8e14847294d2453f575aab1aba0eb54c61 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 25 Oct 2018 18:14:22 +0200 Subject: [PATCH 006/149] 5590: Item move component --- resources/i18n/en.json | 10 ++ .../edit-item-page/edit-item-page.module.ts | 8 +- .../edit-item-page.routing.module.ts | 24 +++- .../item-move/item-move.component.html | 90 +++++++------- .../item-move/item-move.component.ts | 111 ++++++++++++++++++ .../item-operation.component.html | 25 ++-- .../item-operation.component.ts | 13 ++ .../item-operation/itemOperation.model.ts | 12 ++ .../item-status/item-status.component.html | 13 +- .../item-status/item-status.component.ts | 12 +- .../+item-page/item-page-routing.module.ts | 25 ++-- src/app/+search-page/search-page.module.ts | 80 +++++++------ src/app/app-routing.module.ts | 6 +- src/app/core/data/item-data.service.ts | 53 ++++++--- .../input-suggestions.component.ts | 9 ++ 15 files changed, 351 insertions(+), 140 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 5cd54869d2..2d20f21d17 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -103,6 +103,16 @@ "curate": { "head": "Curate" } + }, + "move": { + "head":"Move item: {{id}}", + "description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "inheritpolicies": { + "description": "Inherit the default policies of the destination collection", + "checkbox": "Inherit policies" + }, + "move": "Move", + "cancel": "Cancel" } } }, 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 e7016eb05d..09a5e1d588 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 @@ -4,15 +4,21 @@ import { SharedModule } from '../../shared/shared.module'; import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; import { EditItemPageComponent } from './edit-item-page.component'; import { ItemStatusComponent } from './item-status/item-status.component'; +import {ItemOperationComponent} from './item-operation/item-operation.component'; +import {ItemMoveComponent} from './item-move/item-move.component'; +import {SearchPageModule} from '../../+search-page/search-page.module'; @NgModule({ imports: [ CommonModule, SharedModule, - EditItemPageRoutingModule + EditItemPageRoutingModule, + SearchPageModule.forRoot(), ], declarations: [ EditItemPageComponent, + ItemOperationComponent, + ItemMoveComponent, ItemStatusComponent ] }) 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 46e8dab609..e9b0643cc1 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 @@ -1,7 +1,16 @@ -import { ItemPageResolver } from '../item-page.resolver'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { EditItemPageComponent } from './edit-item-page.component'; +import {ItemPageResolver} from '../item-page.resolver'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {EditItemPageComponent} from './edit-item-page.component'; +import {ItemMoveComponent} from './item-move/item-move.component'; +import {URLCombiner} from '../../core/url-combiner/url-combiner'; +import {getItemEditPath} from '../item-page-routing.module'; + +const ITEM_EDIT_MOVE_PATH = 'move'; + +export function getItemEditMovePath(id: string) { + return new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH); +} @NgModule({ imports: [ @@ -12,6 +21,13 @@ import { EditItemPageComponent } from './edit-item-page.component'; resolve: { item: ItemPageResolver } + }, + { + path: ITEM_EDIT_MOVE_PATH, + component: ItemMoveComponent, + resolve: { + item: ItemPageResolver + } } ]) ], diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index a060aa0fed..fe27ed36a5 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -1,51 +1,41 @@ -import {Component, OnInit} from '@angular/core'; -import {Collection} from '../../../core/shared/collection.model'; -import {RemoteData} from '../../../core/data/remote-data'; -import {Item} from '../../../core/shared/item.model'; -import {getSucceededRemoteData} from '../../../core/shared/operators'; -import {Observable} from 'rxjs'; -import {PaginatedList} from '../../../core/data/paginated-list'; -import {TranslateService} from '@ngx-translate/core'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {SearchService} from '../../../+search-page/search-service/search.service'; -import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service'; -import {ActivatedRoute, Router} from '@angular/router'; -import {CollectionDataService} from '../../../core/data/collection-data.service'; +
+
+
+

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

+

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

+
+
+ + +
+
+
+
+

+ + +

+

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

+
+
-@Component({ - selector: 'ds-item-move', - templateUrl: './item-move.component.html' -}) -export class ItemMoveComponent implements OnInit { - inheritPolicies: boolean; - - itemRD$: Observable>; - - /** - * List of collections to show under the "Browse" tab - * Collections that are mapped to the item - */ - itemCollectionsRD$: Observable>>; - - constructor(private route: ActivatedRoute, - private router: Router, - private searchConfigService: SearchConfigurationService, - private searchService: SearchService, - private notificationsService: NotificationsService, - private collectionDataService: CollectionDataService, - private translateService: TranslateService) { - } - - ngOnInit(): void { - this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; - this.loadCollectionLists(); - } - - /** - * Load all available collections to move the item to. - * TODO: When the API support it, only fetch collections where user has ADD rights to. - */ - loadCollectionLists() { - this.itemCollectionsRD$ = this.collectionDataService.findAll(); - } -} + + +
+
+
diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index e69de29bb2..e0819257c2 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -0,0 +1,111 @@ +import {Component, OnInit} from '@angular/core'; +import {SearchService} from '../../../+search-page/search-service/search.service'; +import {Observable} from 'rxjs/Observable'; +import {map} from 'rxjs/operators'; +import {DSpaceObjectType} from '../../../core/shared/dspace-object-type.model'; +import {SearchOptions} from '../../../+search-page/search-options.model'; +import {RemoteData} from '../../../core/data/remote-data'; +import {DSpaceObject} from '../../../core/shared/dspace-object.model'; +import {PaginatedList} from '../../../core/data/paginated-list'; +import {SearchResult} from '../../../+search-page/search-result.model'; +import {PaginatedSearchOptions} from '../../../+search-page/paginated-search-options.model'; +import {Item} from '../../../core/shared/item.model'; +import {ActivatedRoute, Router} from '@angular/router'; +import {NotificationsService} from '../../../shared/notifications/notifications.service'; +import {CollectionDataService} from '../../../core/data/collection-data.service'; +import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service'; +import {TranslateService} from '@ngx-translate/core'; +import {getSucceededRemoteData} from '../../../core/shared/operators'; +import {ItemDataService} from '../../../core/data/item-data.service'; +import {RestResponse} from '../../../core/cache/response-cache.models'; +import {getItemEditPath} from '../../item-page-routing.module'; + +@Component({ + selector: 'ds-item-move', + templateUrl: './item-move.component.html' +}) +export class ItemMoveComponent implements OnInit { + + inheritPolicies = false; + itemRD$: Observable>; + /** + * Search options + */ + searchOptions$: Observable; + filterSearchResults: Observable = Observable.of([]); + selectedCollection: string; + + selectedCollectionId: string; + itemId: string; + + constructor(private route: ActivatedRoute, + private router: Router, + private notificationsService: NotificationsService, + private collectionDataService: CollectionDataService, + private itemDataService: ItemDataService, + private searchConfigService: SearchConfigurationService, + private searchService: SearchService, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$.first().subscribe((rd) => { + this.itemId = rd.payload.id; + } + ); + this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; + this.loadSuggestions(''); + } + + findSuggestions(query): void { + this.loadSuggestions(query); + } + + /** + * Load all available collections to move the item to. + * TODO: When the API support it, only fetch collections where user has ADD rights to. + */ + loadSuggestions(query): void { + this.filterSearchResults = this.searchService.search(new SearchOptions({ + dsoType: DSpaceObjectType.COLLECTION, + query: query + })).first().pipe( + map((rd: RemoteData>>) => { + return rd.payload.page.map((searchResult) => { + return { + displayValue: searchResult.dspaceObject.name, + value: {name: searchResult.dspaceObject.name, id: searchResult.dspaceObject.uuid} + }; + }); + }) + ); + + } + + onClick(data: any): void { + this.selectedCollection = data.name; + this.selectedCollectionId = data.id; + } + + /** + * @returns {string} the current URL + */ + getCurrentUrl() { + return this.router.url; + } + + moveCollection() { + this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).first().subscribe( + (response: RestResponse) => { + this.router.navigate([getItemEditPath(this.itemId)]); + if (response.isSuccessful) { + this.notificationsService.success(this.translateService.get('item.move.success')); + } else { + this.notificationsService.error(this.translateService.get('item.move.error')); + } + } + ); + + } +} diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html index 59b625d8c0..4623195437 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html @@ -1,10 +1,15 @@ -import {Component} from '@angular/core'; - -@Component({ - selector: 'ds-item-operation', - templateUrl: './ds-item-operation.html' -}) - -export class ItemOperationComponent { - -} +
+ + {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}} + +
+ +
+ + {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} + +
\ No newline at end of file diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts index e69de29bb2..951d66cbd8 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts @@ -0,0 +1,13 @@ +import {Component, Input} from '@angular/core'; +import {ItemOperation} from './itemOperation.model'; + +@Component({ + selector: 'ds-item-operation', + templateUrl: './item-operation.component.html' +}) + +export class ItemOperationComponent { + + @Input() operation: ItemOperation; + +} diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts index e69de29bb2..6a54744fcb 100644 --- a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts +++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts @@ -0,0 +1,12 @@ +export class ItemOperation { + + operationKey: string; + operationUrl: string; + disabled: boolean; + + constructor(operationKey: string, operationUrl: string) { + this.operationKey = operationKey; + this.operationUrl = operationUrl; + } + +} 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 index 78ab9174eb..0f7d9a5607 100644 --- 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 @@ -15,16 +15,7 @@ {{getItemPage()}}
-
-
- - {{'item.edit.tabs.status.buttons.' + actionKey + '.label' | translate}} - -
- +
+
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 bc9dda61ec..e92ae10b55 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 @@ -2,6 +2,7 @@ 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'; +import {ItemOperation} from '../item-operation/itemOperation.model'; @Component({ selector: 'ds-item-status', @@ -35,7 +36,7 @@ export class ItemStatusComponent implements OnInit { * The possible actions that can be performed on the item * key: id value: url to action's component */ - actions: any; + operations: ItemOperation[]; /** * The keys of the actions (to loop over) */ @@ -57,11 +58,10 @@ export class ItemStatusComponent implements OnInit { i18n example: 'item.edit.tabs.status.buttons..label' 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() + '/' - }); - this.actionsKeys = Object.keys(this.actions); + this.operations = [ + new ItemOperation('mappedCollections',this.getCurrentUrl() + '/'), + new ItemOperation('move', this.getCurrentUrl() + '/move'), + ] } /** diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index be31b0a82d..a155d00cc0 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -1,10 +1,21 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import {NgModule} from '@angular/core'; +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 {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 {URLCombiner} from '../core/url-combiner/url-combiner'; +import {getItemModulePath} from '../app-routing.module'; + +export function getItemPageRoute(itemId: string) { + return new URLCombiner(getItemModulePath(), itemId).toString(); +} +export function getItemEditPath(id: string) { + return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString() +} + +const ITEM_EDIT_PATH = ':id/edit'; @NgModule({ imports: [ @@ -25,7 +36,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; } }, { - path: ':id/edit', + path: ITEM_EDIT_PATH, loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', canActivate: [AuthenticatedGuard] } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 0c8a4ee306..a231d8da5a 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -1,38 +1,45 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { CoreModule } from '../core/core.module'; -import { SharedModule } from '../shared/shared.module'; -import { SearchPageRoutingModule } from './search-page-routing.module'; -import { SearchPageComponent } from './search-page.component'; -import { SearchResultsComponent } from './search-results/search-results.component'; -import { ItemSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component'; -import { CollectionSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; -import { CommunitySearchResultListElementComponent } from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; -import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component'; -import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component' -import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; -import { SearchService } from './search-service/search.service'; -import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component'; -import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; -import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects'; -import { SearchSettingsComponent } from './search-settings/search-settings.component'; -import { EffectsModule } from '@ngrx/effects'; -import { SearchFiltersComponent } from './search-filters/search-filters.component'; -import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component'; -import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component'; -import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; -import { SearchLabelsComponent } from './search-labels/search-labels.component'; -import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component'; -import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component'; -import { SearchFacetFilterWrapperComponent } from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component'; -import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component'; -import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component'; -import { SearchConfigurationService } from './search-service/search-configuration.service'; +import {ModuleWithProviders, NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CoreModule} from '../core/core.module'; +import {SharedModule} from '../shared/shared.module'; +import {SearchPageRoutingModule} from './search-page-routing.module'; +import {SearchPageComponent} from './search-page.component'; +import {SearchResultsComponent} from './search-results/search-results.component'; +import {ItemSearchResultListElementComponent} from '../shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component'; +import {CollectionSearchResultListElementComponent} from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; +import {CommunitySearchResultListElementComponent} from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; +import {ItemSearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component'; +import {CommunitySearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; +import {CollectionSearchResultGridElementComponent} from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; +import {SearchService} from './search-service/search.service'; +import {SearchSidebarComponent} from './search-sidebar/search-sidebar.component'; +import {SearchSidebarService} from './search-sidebar/search-sidebar.service'; +import {SearchSidebarEffects} from './search-sidebar/search-sidebar.effects'; +import {SearchSettingsComponent} from './search-settings/search-settings.component'; +import {EffectsModule} from '@ngrx/effects'; +import {SearchFiltersComponent} from './search-filters/search-filters.component'; +import {SearchFilterComponent} from './search-filters/search-filter/search-filter.component'; +import {SearchFacetFilterComponent} from './search-filters/search-filter/search-facet-filter/search-facet-filter.component'; +import {SearchFilterService} from './search-filters/search-filter/search-filter.service'; +import {SearchLabelsComponent} from './search-labels/search-labels.component'; +import {SearchRangeFilterComponent} from './search-filters/search-filter/search-range-filter/search-range-filter.component'; +import {SearchTextFilterComponent} from './search-filters/search-filter/search-text-filter/search-text-filter.component'; +import {SearchFacetFilterWrapperComponent} from './search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component'; +import {SearchBooleanFilterComponent} from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component'; +import {SearchHierarchyFilterComponent} from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component'; +import {SearchConfigurationService} from './search-service/search-configuration.service'; const effects = [ SearchSidebarEffects ]; +const PROVIDERS = [ + SearchService, + SearchSidebarService, + SearchFilterService, + SearchConfigurationService +]; + @NgModule({ imports: [ SearchPageRoutingModule, @@ -65,10 +72,7 @@ const effects = [ SearchBooleanFilterComponent, ], providers: [ - SearchService, - SearchSidebarService, - SearchFilterService, - SearchConfigurationService + ...PROVIDERS ], entryComponents: [ ItemSearchResultListElementComponent, @@ -89,4 +93,12 @@ const effects = [ * This module handles all components and pipes that are necessary for the search page */ export class SearchPageModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: CoreModule, + providers: [ + ...PROVIDERS + ] + }; + } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7de83651ff..e7ea10598d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,6 +3,10 @@ import { RouterModule } from '@angular/router'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; +const ITEM_MODULE_PATH = 'items'; +export function getItemModulePath() { + return `/${ITEM_MODULE_PATH}`; +} @NgModule({ imports: [ RouterModule.forRoot([ @@ -10,7 +14,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' }, { path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, - { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' }, + { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' }, { path: 'admin', loadChildren: './+admin/admin.module#AdminModule' }, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f984dceb12..90311a6f82 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,21 +1,24 @@ -import { Inject, Injectable } from '@angular/core'; +import {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 { BrowseService } from '../browse/browse.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedItem } from '../cache/models/normalized-item.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { CoreState } from '../core.reducers'; -import { Item } from '../shared/item.model'; -import { URLCombiner } from '../url-combiner/url-combiner'; +import {Store} from '@ngrx/store'; +import {Observable} from 'rxjs/Observable'; +import {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'; +import {ResponseCacheService} from '../cache/response-cache.service'; +import {CoreState} from '../core.reducers'; +import {Item} from '../shared/item.model'; +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 {DataService} from './data.service'; +import {RequestService} from './request.service'; +import {HALEndpointService} from '../shared/hal-endpoint.service'; +import {FindAllOptions, PostRequest, RestRequest} from './request.models'; +import {distinctUntilChanged, map} from 'rxjs/operators'; +import {RestResponse} from '../cache/response-cache.models'; +import {configureRequest, getResponseFromSelflink} from '../shared/operators'; +import {ResponseCacheEntry} from '../cache/response-cache.reducer'; @Injectable() export class ItemDataService extends DataService { @@ -48,4 +51,22 @@ export class ItemDataService extends DataService { .distinctUntilChanged(); } + public getMoveItemEndpoint(itemId: string, collectionId?: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)), + map((endpoint: string) => `${endpoint}/owningCollection/move/${collectionId ? `/${collectionId}` : ''}`) + ); + } + + public moveToCollection(itemId: string, collectionId: string): Observable { + return this.getMoveItemEndpoint(itemId, collectionId).pipe( + // isNotEmptyOperator(), + distinctUntilChanged(), + map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), + configureRequest(this.requestService), + map((request: RestRequest) => request.href), + getResponseFromSelflink(this.responseCache), + map((responseCacheEntry: ResponseCacheEntry) => responseCacheEntry.response) + ); + } } diff --git a/src/app/shared/input-suggestions/input-suggestions.component.ts b/src/app/shared/input-suggestions/input-suggestions.component.ts index eb28583eaa..ae15a805e9 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.ts +++ b/src/app/shared/input-suggestions/input-suggestions.component.ts @@ -159,6 +159,15 @@ export class InputSuggestionsComponent { this.show.next(false); } + /** + * Changes the show variable so the suggestion dropdown opens + */ + open() { + if (!this.blockReopen) { + this.show.next(true); + } + } + /** * For usage of the isNotEmpty function in the template */ From 4411c312e5861b4e3702e6325a34dfd306f692c7 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 26 Nov 2018 16:28:39 +0100 Subject: [PATCH 007/149] 55990: Add move item component --- .../item-move/item-move.component.html | 2 +- .../item-move/item-move.component.spec.ts | 179 ++++++++++++++++++ .../item-move/item-move.component.ts | 28 +-- .../item-operation.component.spec.ts | 44 +++++ .../item-operation.component.ts | 4 +- .../item-operation/itemOperation.model.ts | 9 + .../item-status/item-status.component.spec.ts | 3 +- .../search-hierarchy-filter.component.html | 2 +- .../search-text-filter.component.html | 2 +- 9 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index fe27ed36a5..0c97628d4c 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -6,7 +6,7 @@
; + +const mockItem = Object.assign(new Item(), { + id: 'fake-id', + handle: 'fake/handle', + lastModified: '2018' +}); + +const itemPageUrl = `fake-url/${mockItem.id}`; +const routerStub = Object.assign(new RouterStub(), { + url: `${itemPageUrl}/edit` +}); + +const mockItemDataService = jasmine.createSpyObj({ + moveToCollection: Observable.of(new RestResponse(true, '200')) +}); + +const mockItemDataServiceFail = jasmine.createSpyObj({ + moveToCollection: Observable.of(new RestResponse(false, '500')) +}); + +const routeStub = { + data: Observable.of({ + item: new RemoteData(false, false, true, null, { + id: 'item1' + }) + }) +}; + +const mockSearchService = { + search: () => { + return Observable.of(new RemoteData(false, false, true, null, + new PaginatedList(null, [ + { + dspaceObject: { + name: 'Test collection 1', + uuid: 'collection1' + }, hitHighlights: {} + }, { + dspaceObject: { + name: 'Test collection 2', + uuid: 'collection2' + }, hitHighlights: {} + } + ]))); + } +}; + +const notificationsServiceStub = new NotificationsServiceStub(); + +describe('ItemMoveComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [ItemMoveComponent], + providers: [ + {provide: ActivatedRoute, useValue: routeStub}, + {provide: Router, useValue: routerStub}, + {provide: ItemDataService, useValue: mockItemDataService}, + {provide: NotificationsService, useValue: notificationsServiceStub}, + {provide: SearchService, useValue: mockSearchService}, + ], schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemMoveComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should load suggestions', () => { + const expected = [ + { + displayValue: 'Test collection 1', + value: { + name: 'Test collection 1', + id: 'collection1', + } + }, + { + displayValue: 'Test collection 2', + value: { + name: 'Test collection 2', + id: 'collection2', + } + } + ]; + + comp.CollectionSearchResults.subscribe((value) => { + expect(value).toEqual(expected); + } + ); + }); + it('should get current url ', () => { + expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit'); + }); + it('should on click select the correct collection name and id', () => { + const data = { + name: 'Test collection 1', + id: 'collection1', + }; + comp.onClick(data); + + expect(comp.selectedCollection).toEqual('Test collection 1'); + expect(comp.selectedCollectionId).toEqual('collection1'); + }); + describe('moveCollection', () => { + it('should call itemDataService.moveToCollection', () => { + comp.itemId = 'item-id'; + comp.selectedCollectionId = 'selected-collection-id'; + comp.moveCollection(); + + expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', 'selected-collection-id'); + }); + it('should call notificationsService success message on success', () => { + spyOn(notificationsServiceStub, 'success'); + + comp.moveCollection(); + + expect(notificationsServiceStub.success).toHaveBeenCalled(); + }); + }); +}); + +describe('ItemMoveComponent fail', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [ItemMoveComponent], + providers: [ + {provide: ActivatedRoute, useValue: routeStub}, + {provide: Router, useValue: routerStub}, + {provide: ItemDataService, useValue: mockItemDataServiceFail}, + {provide: NotificationsService, useValue: notificationsServiceStub}, + {provide: SearchService, useValue: mockSearchService}, + ], schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemMoveComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should call notificationsService error message on fail', () => { + spyOn(notificationsServiceStub, 'error'); + + comp.moveCollection(); + + expect(notificationsServiceStub.error).toHaveBeenCalled(); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index e0819257c2..338d4b96a3 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -8,12 +8,9 @@ import {RemoteData} from '../../../core/data/remote-data'; import {DSpaceObject} from '../../../core/shared/dspace-object.model'; import {PaginatedList} from '../../../core/data/paginated-list'; import {SearchResult} from '../../../+search-page/search-result.model'; -import {PaginatedSearchOptions} from '../../../+search-page/paginated-search-options.model'; import {Item} from '../../../core/shared/item.model'; import {ActivatedRoute, Router} from '@angular/router'; import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {CollectionDataService} from '../../../core/data/collection-data.service'; -import {SearchConfigurationService} from '../../../+search-page/search-service/search-configuration.service'; import {TranslateService} from '@ngx-translate/core'; import {getSucceededRemoteData} from '../../../core/shared/operators'; import {ItemDataService} from '../../../core/data/item-data.service'; @@ -24,15 +21,14 @@ import {getItemEditPath} from '../../item-page-routing.module'; selector: 'ds-item-move', templateUrl: './item-move.component.html' }) +/** + * Component that handles the moving of an item to a different collection + */ export class ItemMoveComponent implements OnInit { inheritPolicies = false; itemRD$: Observable>; - /** - * Search options - */ - searchOptions$: Observable; - filterSearchResults: Observable = Observable.of([]); + CollectionSearchResults: Observable = Observable.of([]); selectedCollection: string; selectedCollectionId: string; @@ -41,9 +37,7 @@ export class ItemMoveComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, private notificationsService: NotificationsService, - private collectionDataService: CollectionDataService, private itemDataService: ItemDataService, - private searchConfigService: SearchConfigurationService, private searchService: SearchService, private translateService: TranslateService) { } @@ -54,10 +48,13 @@ export class ItemMoveComponent implements OnInit { this.itemId = rd.payload.id; } ); - this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadSuggestions(''); } + /** + * Find suggestions based on entered query + * @param query - Search query + */ findSuggestions(query): void { this.loadSuggestions(query); } @@ -67,7 +64,7 @@ export class ItemMoveComponent implements OnInit { * TODO: When the API support it, only fetch collections where user has ADD rights to. */ loadSuggestions(query): void { - this.filterSearchResults = this.searchService.search(new SearchOptions({ + this.CollectionSearchResults = this.searchService.search(new SearchOptions({ dsoType: DSpaceObjectType.COLLECTION, query: query })).first().pipe( @@ -83,6 +80,10 @@ export class ItemMoveComponent implements OnInit { } + /** + * Set the collection name and id based on the selected value + * @param data - obtained from the ds-input-suggestions component + */ onClick(data: any): void { this.selectedCollection = data.name; this.selectedCollectionId = data.id; @@ -95,6 +96,9 @@ export class ItemMoveComponent implements OnInit { return this.router.url; } + /** + * Moves the item to a new collection based on the selected collection + */ moveCollection() { this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).first().subscribe( (response: RestResponse) => { diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts new file mode 100644 index 0000000000..092f3af0ac --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -0,0 +1,44 @@ +import {ItemOperation} from './itemOperation.model'; +import {async, TestBed} from '@angular/core/testing'; +import {ItemOperationComponent} from './item-operation.component'; +import {TranslateModule} from '@ngx-translate/core'; +import {By} from '@angular/platform-browser'; + +describe('ItemOperationComponent', () => { + const itemOperation: ItemOperation = new ItemOperation('key1', 'url1'); + + let fixture; + let comp; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemOperationComponent] + }).compileComponents(); + })); + + beforeEach(() => { + + fixture = TestBed.createComponent(ItemOperationComponent); + comp = fixture.componentInstance; + comp.operation = itemOperation; + fixture.detectChanges(); + }); + + it('should render operation row', () => { + const span = fixture.debugElement.query(By.css('span')).nativeElement; + expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); + const link = fixture.debugElement.query(By.css('a')).nativeElement; + expect(link.href).toContain('url1'); + expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + }); + it('should render disabled operation row', () => { + itemOperation.setDisabled(true); + fixture.detectChanges(); + + const span = fixture.debugElement.query(By.css('span')).nativeElement; + expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); + const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement; + expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts index 951d66cbd8..76d056df95 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts @@ -5,7 +5,9 @@ import {ItemOperation} from './itemOperation.model'; selector: 'ds-item-operation', templateUrl: './item-operation.component.html' }) - +/** + * Operation that can be performed on an item + */ export class ItemOperationComponent { @Input() operation: ItemOperation; diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts index 6a54744fcb..0104dfbdb3 100644 --- a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts +++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts @@ -7,6 +7,15 @@ export class ItemOperation { constructor(operationKey: string, operationUrl: string) { this.operationKey = operationKey; this.operationUrl = operationUrl; + this.setDisabled(false); + } + + /** + * Set whether this operation should be disabled + * @param disabled + */ + setDisabled(disabled: boolean): void { + this.disabled = disabled; } } 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 index 2df4b977cb..319d4c47ae 100644 --- 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 @@ -10,6 +10,7 @@ import { Router } from '@angular/router'; import { RouterStub } from '../../../shared/testing/router-stub'; import { Item } from '../../../core/shared/item.model'; import { By } from '@angular/platform-browser'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; describe('ItemStatusComponent', () => { let comp: ItemStatusComponent; @@ -33,7 +34,7 @@ describe('ItemStatusComponent', () => { providers: [ { provide: Router, useValue: routerStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) } - ] + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 812f543716..962d09e6c4 100644 --- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -30,7 +30,7 @@ | translate}}
-
- Date: Tue, 27 Nov 2018 14:38:19 +0100 Subject: [PATCH 008/149] 55990: Fix param name --- .../edit-item-page/item-move/item-move.component.html | 2 +- .../edit-item-page/item-move/item-move.component.spec.ts | 2 +- .../edit-item-page/item-move/item-move.component.ts | 4 ++-- .../search-hierarchy-filter.component.html | 2 +- .../search-text-filter/search-text-filter.component.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index 0c97628d4c..0a8e4b68b5 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -6,7 +6,7 @@
{ } ]; - comp.CollectionSearchResults.subscribe((value) => { + comp.collectionSearchResults.subscribe((value) => { expect(value).toEqual(expected); } ); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 338d4b96a3..359c04c8e7 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -28,7 +28,7 @@ export class ItemMoveComponent implements OnInit { inheritPolicies = false; itemRD$: Observable>; - CollectionSearchResults: Observable = Observable.of([]); + collectionSearchResults: Observable = Observable.of([]); selectedCollection: string; selectedCollectionId: string; @@ -64,7 +64,7 @@ export class ItemMoveComponent implements OnInit { * TODO: When the API support it, only fetch collections where user has ADD rights to. */ loadSuggestions(query): void { - this.CollectionSearchResults = this.searchService.search(new SearchOptions({ + this.collectionSearchResults = this.searchService.search(new SearchOptions({ dsoType: DSpaceObjectType.COLLECTION, query: query })).first().pipe( diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 962d09e6c4..812f543716 100644 --- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -30,7 +30,7 @@ | translate}}
-
- Date: Thu, 29 Nov 2018 11:39:57 +0100 Subject: [PATCH 009/149] 55990: Move item component - fix message --- resources/i18n/en.json | 1 + .../edit-item-page/item-move/item-move.component.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 395838c289..3cdf0bc180 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -107,6 +107,7 @@ "move": { "head":"Move item: {{id}}", "description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "search.placeholder": "Enter a search query to look for collections", "inheritpolicies": { "description": "Inherit the default policies of the destination collection", "checkbox": "Inherit policies" diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index 0a8e4b68b5..b3627e65bb 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -7,7 +7,7 @@
Date: Fri, 30 Nov 2018 11:18:57 +0100 Subject: [PATCH 010/149] 55990: Item move - tweaks --- resources/i18n/en.json | 4 +++- .../edit-item-page/item-move/item-move.component.html | 4 ++-- .../edit-item-page/item-move/item-move.component.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 3cdf0bc180..cf87e423a7 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -113,7 +113,9 @@ "checkbox": "Inherit policies" }, "move": "Move", - "cancel": "Cancel" + "cancel": "Cancel", + "success": "The item has been moved succesfully", + "error": "An error occured when attempting to move the item" } } }, diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.html b/src/app/+item-page/edit-item-page/item-move/item-move.component.html index b3627e65bb..063028c719 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.html @@ -1,7 +1,7 @@
-

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

+

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

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

@@ -33,7 +33,7 @@ -
diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 359c04c8e7..07894c4504 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -104,9 +104,9 @@ export class ItemMoveComponent implements OnInit { (response: RestResponse) => { this.router.navigate([getItemEditPath(this.itemId)]); if (response.isSuccessful) { - this.notificationsService.success(this.translateService.get('item.move.success')); + this.notificationsService.success(this.translateService.get('item.edit.move.success')); } else { - this.notificationsService.error(this.translateService.get('item.move.error')); + this.notificationsService.error(this.translateService.get('item.edit.move.error')); } } ); From a698b56ae804ab3a51726c1a4170942da4d561cb Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 6 Dec 2018 10:42:37 +0100 Subject: [PATCH 011/149] 55990: Fix issues to move item component after master merge --- .../edit-item-page.component.ts | 11 +++++----- .../item-move/item-move.component.spec.ts | 13 ++++++------ .../item-move/item-move.component.ts | 21 ++++++++++++------- .../item-operation.component.spec.ts | 8 +++---- src/app/core/data/item-data.service.ts | 7 +++---- 5 files changed, 32 insertions(+), 28 deletions(-) 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..de40239b3e 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 { RemoteData } from '../../core/data/remote-data'; -import { Item } from '../../core/shared/item.model'; +import {fadeIn, fadeInOut} from '../../shared/animations/fade'; +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-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index c61a9ed0d3..eaf8e15fa4 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -10,14 +10,13 @@ import {ItemMoveComponent} from './item-move.component'; import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; import {NotificationsService} from '../../../shared/notifications/notifications.service'; import {SearchService} from '../../../+search-page/search-service/search.service'; -import {Observable} from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {of as observableOf} from 'rxjs'; import {FormsModule} from '@angular/forms'; import {ItemDataService} from '../../../core/data/item-data.service'; import {RestResponse} from '../../../core/cache/response-cache.models'; import {RemoteData} from '../../../core/data/remote-data'; import {PaginatedList} from '../../../core/data/paginated-list'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; let comp: ItemMoveComponent; let fixture: ComponentFixture; @@ -34,15 +33,15 @@ const routerStub = Object.assign(new RouterStub(), { }); const mockItemDataService = jasmine.createSpyObj({ - moveToCollection: Observable.of(new RestResponse(true, '200')) + moveToCollection: observableOf(new RestResponse(true, '200')) }); const mockItemDataServiceFail = jasmine.createSpyObj({ - moveToCollection: Observable.of(new RestResponse(false, '500')) + moveToCollection: observableOf(new RestResponse(false, '500')) }); const routeStub = { - data: Observable.of({ + data: observableOf({ item: new RemoteData(false, false, true, null, { id: 'item1' }) @@ -51,7 +50,7 @@ const routeStub = { const mockSearchService = { search: () => { - return Observable.of(new RemoteData(false, false, true, null, + return observableOf(new RemoteData(false, false, true, null, new PaginatedList(null, [ { dspaceObject: { diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 07894c4504..9147ae2238 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -1,7 +1,6 @@ import {Component, OnInit} from '@angular/core'; import {SearchService} from '../../../+search-page/search-service/search.service'; -import {Observable} from 'rxjs/Observable'; -import {map} from 'rxjs/operators'; +import {first, map} from 'rxjs/operators'; import {DSpaceObjectType} from '../../../core/shared/dspace-object-type.model'; import {SearchOptions} from '../../../+search-page/search-options.model'; import {RemoteData} from '../../../core/data/remote-data'; @@ -16,6 +15,8 @@ import {getSucceededRemoteData} from '../../../core/shared/operators'; import {ItemDataService} from '../../../core/data/item-data.service'; import {RestResponse} from '../../../core/cache/response-cache.models'; import {getItemEditPath} from '../../item-page-routing.module'; +import {Observable} from 'rxjs'; +import {of as observableOf} from 'rxjs'; @Component({ selector: 'ds-item-move', @@ -25,10 +26,13 @@ import {getItemEditPath} from '../../item-page-routing.module'; * Component that handles the moving of an item to a different collection */ export class ItemMoveComponent implements OnInit { - + /** + * TODO: There is currently no backend support to change the owningCollection and inherit policies, + * TODO: when this is added, the inherit policies option should be used. + */ inheritPolicies = false; itemRD$: Observable>; - collectionSearchResults: Observable = Observable.of([]); + collectionSearchResults: Observable = observableOf([]); selectedCollection: string; selectedCollectionId: string; @@ -43,8 +47,8 @@ export class ItemMoveComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.map((data) => data.item).pipe(getSucceededRemoteData()) as Observable>; - this.itemRD$.first().subscribe((rd) => { + this.itemRD$ = this.route.data.pipe(map((data) => data.item),getSucceededRemoteData()) as Observable>; + this.itemRD$.subscribe((rd) => { this.itemId = rd.payload.id; } ); @@ -67,7 +71,8 @@ export class ItemMoveComponent implements OnInit { this.collectionSearchResults = this.searchService.search(new SearchOptions({ dsoType: DSpaceObjectType.COLLECTION, query: query - })).first().pipe( + })).pipe( + first(), map((rd: RemoteData>>) => { return rd.payload.page.map((searchResult) => { return { @@ -100,7 +105,7 @@ export class ItemMoveComponent implements OnInit { * Moves the item to a new collection based on the selected collection */ moveCollection() { - this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).first().subscribe( + this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).pipe(first()).subscribe( (response: RestResponse) => { this.router.navigate([getItemEditPath(this.itemId)]); if (response.isSuccessful) { diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 092f3af0ac..15feb5aeda 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -4,11 +4,11 @@ import {ItemOperationComponent} from './item-operation.component'; import {TranslateModule} from '@ngx-translate/core'; import {By} from '@angular/platform-browser'; -describe('ItemOperationComponent', () => { - const itemOperation: ItemOperation = new ItemOperation('key1', 'url1'); +const itemOperation: ItemOperation = new ItemOperation('key1', 'url1'); - let fixture; - let comp; +let fixture; +let comp; +describe('ItemOperationComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index d6c76bff2b..84eca23507 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,7 +1,6 @@ import {Injectable} from '@angular/core'; - import {distinctUntilChanged, map, filter} from 'rxjs/operators'; -import { Injectable } from '@angular/core';import {Store} from '@ngrx/store'; +import {Store} from '@ngrx/store'; import {Observable} from 'rxjs'; import {isNotEmpty, isNotEmptyOperator} from '../../shared/empty.util'; import {BrowseService} from '../browse/browse.service'; @@ -15,7 +14,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 {FindAllOptions, PostRequest, PutRequest, RestRequest} from './request.models'; import {RestResponse} from '../cache/response-cache.models'; import {configureRequest, getResponseFromSelflink} from '../shared/operators'; import {ResponseCacheEntry} from '../cache/response-cache.reducer'; @@ -62,7 +61,7 @@ export class ItemDataService extends DataService { return this.getMoveItemEndpoint(itemId, collectionId).pipe( // isNotEmptyOperator(), distinctUntilChanged(), - map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL)), + map((endpointURL: string) => new PutRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), From e2420c56d38cb3d34d8738985acb991d9dc9dd73 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 6 Dec 2018 15:59:45 +0100 Subject: [PATCH 012/149] Fix item opertation test issue --- .../item-operation/item-operation.component.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 15feb5aeda..54d5a8fe4a 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -4,11 +4,11 @@ import {ItemOperationComponent} from './item-operation.component'; import {TranslateModule} from '@ngx-translate/core'; import {By} from '@angular/platform-browser'; -const itemOperation: ItemOperation = new ItemOperation('key1', 'url1'); - -let fixture; -let comp; describe('ItemOperationComponent', () => { + let itemOperation: ItemOperation; + + let fixture; + let comp; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -19,6 +19,8 @@ describe('ItemOperationComponent', () => { beforeEach(() => { + itemOperation = new ItemOperation('key1', 'url1'); + fixture = TestBed.createComponent(ItemOperationComponent); comp = fixture.componentInstance; comp.operation = itemOperation; From 1e6226205083dcc02c50b2320c4d77be9c0f5933 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 19 Dec 2018 13:20:42 +0100 Subject: [PATCH 013/149] Remove unrelated ItemOperation --- .../edit-item-page/item-status/item-status.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e92ae10b55..70f7c737f6 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 @@ -59,8 +59,7 @@ export class ItemStatusComponent implements OnInit { The value is supposed to be a href for the button */ this.operations = [ - new ItemOperation('mappedCollections',this.getCurrentUrl() + '/'), - new ItemOperation('move', this.getCurrentUrl() + '/move'), + new ItemOperation('move', this.getCurrentUrl() + '/move') ] } From cc862af2498885257a0e6187a2e3b761c0b33450 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 2 Apr 2019 17:36:51 +0200 Subject: [PATCH 014/149] 61142: Item relations initial edit page --- resources/i18n/en.json | 4 + .../edit-item-page/edit-item-page.module.ts | 6 +- .../edit-item-page.routing.module.ts | 6 + .../edit-in-place-relationship.component.html | 19 ++ .../edit-in-place-relationship.component.scss | 15 ++ ...it-in-place-relationship.component.spec.ts | 0 .../edit-in-place-relationship.component.ts | 64 +++++ .../item-relationships.component.html | 33 +++ .../item-relationships.component.scss | 22 ++ .../item-relationships.component.spec.ts | 0 .../item-relationships.component.ts | 228 ++++++++++++++++++ .../object-updates/object-updates.service.ts | 15 ++ 12 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts create mode 100644 src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html create mode 100644 src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss create mode 100644 src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 1952e345d8..8b7d15abfd 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -175,6 +175,10 @@ "head": "Item Metadata", "title": "Item Edit - Metadata" }, + "relationships": { + "head": "Item Relationships", + "title": "Item Edit - Relationships" + }, "view": { "head": "View Item", "title": "Item Edit - View" 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 0c1de642ce..079f065d5f 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 @@ -15,6 +15,8 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; +import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; +import { EditInPlaceRelationshipComponent } from './item-relationships/edit-in-place-relationship/edit-in-place-relationship.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -37,8 +39,10 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo ItemDeleteComponent, ItemStatusComponent, ItemMetadataComponent, + ItemRelationshipsComponent, ItemBitstreamsComponent, - EditInPlaceFieldComponent + EditInPlaceFieldComponent, + EditInPlaceRelationshipComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 223b5f7c8e..55c5bcb747 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -10,6 +10,7 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; +import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -49,6 +50,11 @@ const ITEM_EDIT_DELETE_PATH = 'delete'; component: ItemMetadataComponent, data: { title: 'item.edit.tabs.metadata.title' } }, + { + path: 'relationships', + component: ItemRelationshipsComponent, + data: { title: 'item.edit.tabs.relationships.title' } + }, { path: 'view', /* TODO - change when view page exists */ diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html new file mode 100644 index 0000000000..84c645d1a8 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
+
+ + +
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss new file mode 100644 index 0000000000..808a8344ba --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss @@ -0,0 +1,15 @@ +@import '../../../../../styles/variables.scss'; + +.btn[disabled] { + color: $gray-600; + border-color: $gray-600; + z-index: 0; // prevent border colors jumping on hover +} + +.relationship-action-buttons { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts new file mode 100644 index 0000000000..bb29f7a889 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts @@ -0,0 +1,64 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; +import { cloneDeep } from 'lodash'; +import { Item } from '../../../../core/shared/item.model'; +import { VIEW_MODE_ELEMENT } from '../../../simple/related-items/related-items-component'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; +import { Observable } from 'rxjs/internal/Observable'; +import { map } from 'rxjs/operators'; + +@Component({ + // tslint:disable-next-line:component-selector + selector: '[ds-edit-in-place-relationship]', + styleUrls: ['./edit-in-place-relationship.component.scss'], + templateUrl: './edit-in-place-relationship.component.html', +}) +export class EditInPlaceRelationshipComponent implements OnChanges { + /** + * The current field, value and state of the relationship + */ + @Input() fieldUpdate: FieldUpdate; + + /** + * The current url of this page + */ + @Input() url: string; + + /** + * The related item of this relationship + */ + item: Item; + + /** + * The view-mode we're currently on + */ + viewMode = VIEW_MODE_ELEMENT; + + constructor(private objectUpdatesService: ObjectUpdatesService) { + } + + /** + * Sets the current relationship based on the fieldUpdate input field + */ + ngOnChanges(): void { + this.item = cloneDeep(this.fieldUpdate.field) as Item; + } + + remove(): void { + this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.item); + } + + undo(): void { + this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.item.uuid); + } + + canRemove(): boolean { + return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; + } + + canUndo(): boolean { + return this.fieldUpdate.changeType >= 0; + } + +} diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html new file mode 100644 index 0000000000..c95812b4bb --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -0,0 +1,33 @@ +
+
+ + + +
+
+
{{label}}
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss new file mode 100644 index 0000000000..898533a9f0 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss @@ -0,0 +1,22 @@ +@import '../../../../styles/variables.scss'; + +.button-row { + .btn { + margin-right: 0.5 * $spacer; + + &:last-child { + margin-right: 0; + } + + @media screen and (min-width: map-get($grid-breakpoints, sm)) { + min-width: $edit-item-button-min-width; + } + } + + &.top .btn { + margin-top: $spacer/2; + margin-bottom: $spacer/2; + } + + +} 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 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts new file mode 100644 index 0000000000..0bd1635ae1 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -0,0 +1,228 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { Observable } from 'rxjs/internal/Observable'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { distinctUntilChanged, filter, first, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { zip as observableZip } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; +import { hasValue, hasValueOperator } from '../../../shared/empty.util'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; +import { + compareArraysUsingIds, + filterRelationsByTypeLabel, + relationsToItems +} from '../../simple/item-types/shared/item.component'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; +import { TranslateService } from '@ngx-translate/core'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; + +@Component({ + selector: 'ds-item-relationships', + styleUrls: ['./item-relationships.component.scss'], + templateUrl: './item-relationships.component.html', +}) +export class ItemRelationshipsComponent implements OnInit { + + /** + * The item to display the edit page for + */ + item: Item; + /** + * The current values and updates for all this item's metadata fields + */ + updates$: Observable; + /** + * The current url of this page + */ + url: string; + /** + * Prefix for this component's notification translate keys + */ + private notificationsPrefix = 'item.edit.metadata.notifications.'; + + /** + * The labels of all different relations within this item + */ + relationLabels$: Observable; + + /** + * Resolved relationships and types together in one observable + */ + resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>; + /** + * The time span for being able to undo discarding changes + */ + private discardTimeOut: number; + + constructor(private route: ActivatedRoute, + private router: Router, + private translateService: TranslateService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private objectUpdatesService: ObjectUpdatesService, + private notificationsService: NotificationsService, + private itemDataService: ItemDataService) { + } + + ngOnInit(): void { + this.route.parent.data.pipe(map((data) => data.item)) + .pipe( + first(), + map((data: RemoteData) => data.payload) + ).subscribe((item: Item) => { + this.item = item; + }); + this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; + this.url = this.router.url; + if (this.url.indexOf('?') > 0) { + this.url = this.url.substr(0, this.url.indexOf('?')); + } + this.hasChanges().pipe(first()).subscribe((hasChanges) => { + if (!hasChanges) { + this.initializeOriginalFields(); + } else { + this.checkLastModified(); + } + }); + this.updates$ = this.getRelationships().pipe( + relationsToItems(this.item.id, this.itemDataService), + switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items)) + ); + this.initRelationshipObservables(); + } + + initRelationshipObservables() { + const relationships$ = this.getRelationships(); + + const relationshipTypes$ = relationships$.pipe( + flatMap((rels: Relationship[]) => + observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe( + map(([...arr]: Array>) => arr.map((d: RemoteData) => d.payload).filter((type) => hasValue(type))) + ) + ), + distinctUntilChanged(compareArraysUsingIds()) + ); + + this.resolvedRelsAndTypes$ = observableCombineLatest( + relationships$, + relationshipTypes$ + ); + this.relationLabels$ = relationshipTypes$.pipe( + map((types: RelationshipType[]) => Array.from(new Set(types.map((type) => type.leftLabel)))) + ); + } + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackUpdate(index, update: FieldUpdate) { + return update && update.field ? update.field.uuid : undefined; + } + + /** + * Checks whether or not there are currently updates for this item + */ + hasChanges(): Observable { + return this.objectUpdatesService.hasUpdates(this.url); + } + + /** + * Checks whether or not the item is currently reinstatable + */ + isReinstatable(): Observable { + return this.objectUpdatesService.isReinstatable(this.url); + } + + discard(): void { + const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); + this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); + } + + /** + * Request the object updates service to undo discarding all changes to this item + */ + reinstate() { + this.objectUpdatesService.reinstateFieldUpdates(this.url); + } + + submit(): void { + const updatedItems$ = this.getRelationships().pipe( + first(), + relationsToItems(this.item.id, this.itemDataService), + switchMap((items: Item[]) => this.objectUpdatesService.getUpdatedFields(this.url, items) as Observable) + ); + // TODO: Delete relationships + } + + private initializeOriginalFields() { + this.getRelationships().pipe( + first(), + relationsToItems(this.item.id, this.itemDataService) + ).subscribe((items: Item[]) => { + this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); + }); + } + + /** + * Checks if the current item is still in sync with the version in the store + * If it's not, a notification is shown and the changes are removed + */ + private checkLastModified() { + const currentVersion = this.item.lastModified; + this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( + (updateVersion: Date) => { + if (updateVersion.getDate() !== currentVersion.getDate()) { + this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); + this.initializeOriginalFields(); + } + } + ); + } + + public getRelationships(): Observable { + return this.item.relationships.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((rels: PaginatedList) => rels.page), + hasValueOperator(), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + public getRelatedItemsByLabel(label: string): Observable { + return this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel(label), + relationsToItems(this.item.id, this.itemDataService) + ); + } + + public getUpdatesByLabel(label: string): Observable { + return this.getRelatedItemsByLabel(label).pipe( + switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) + ) + } + + /** + * Get translated notification title + * @param key + */ + private getNotificationTitle(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.title'); + } + + /** + * Get translated notification content + * @param key + */ + private getNotificationContent(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.content'); + + } + +} diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index a13fb9487b..6ef3ca91ca 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -103,6 +103,21 @@ export class ObjectUpdatesService { })) } + getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { + const objectUpdates = this.getObjectEntry(url); + return objectUpdates.pipe(map((objectEntry) => { + const fieldUpdates: FieldUpdates = {}; + for (const object of initialFields) { + let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; + if (isEmpty(fieldUpdate)) { + fieldUpdate = { field: object, changeType: undefined }; + } + fieldUpdates[object.uuid] = fieldUpdate; + } + return fieldUpdates; + })) + } + /** * Method to check if a specific field is currently editable in the store * @param url The URL of the page on which the field resides From 4a749cf91de89223b93e301d89bb685f01615c20 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Apr 2019 11:58:45 +0200 Subject: [PATCH 015/149] 61142: AbstractItemUpdate component and refactoring of item-metadata and item-relationships --- resources/i18n/en.json | 25 +++ .../abstract-item-update.component.ts | 174 ++++++++++++++++++ .../item-metadata/item-metadata.component.ts | 152 ++------------- .../item-relationships.component.html | 2 +- .../item-relationships.component.ts | 159 ++++------------ 5 files changed, 254 insertions(+), 258 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 8b7d15abfd..b64edd42d5 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -273,6 +273,31 @@ "content": "Your changes to this item's metadata were saved." } } + }, + "relationships": { + "discard-button": "Discard", + "reinstate-button": "Undo", + "save-button": "Save", + "edit": { + "buttons": { + "remove": "Remove", + "undo": "Undo changes" + } + }, + "notifications": { + "outdated": { + "title": "Changed outdated", + "content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts" + }, + "discarded": { + "title": "Changed discarded", + "content": "Your changes were discarded. To reinstate your changes click the 'Undo' button" + }, + "saved": { + "title": "Relationships saved", + "content": "Your changes to this item's relationships were saved." + } + } } } }, diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts new file mode 100644 index 0000000000..3cc2a5ed84 --- /dev/null +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -0,0 +1,174 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../../../core/shared/item.model'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; +import { first, map } from 'rxjs/operators'; +import { RemoteData } from '../../../core/data/remote-data'; + +@Component({ + selector: 'ds-abstract-item-update', + template: ``, +}) +/** + * Abstract component for managing object updates of an item + */ +export abstract class AbstractItemUpdateComponent implements OnInit { + /** + * The item to display the edit page for + */ + protected item: Item; + /** + * The current values and updates for all this item's metadata fields + */ + protected updates$: Observable; + /** + * The current url of this page + */ + protected url: string; + /** + * Prefix for this component's notification translate keys + */ + protected notificationsPrefix; + /** + * The time span for being able to undo discarding changes + */ + protected discardTimeOut: number; + + constructor( + protected itemService: ItemDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected router: Router, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected route: ActivatedRoute + ) { + + } + + /** + * Initialize common properties between item-update components + */ + ngOnInit(): void { + this.route.parent.data.pipe(map((data) => data.item)) + .pipe( + first(), + map((data: RemoteData) => data.payload) + ).subscribe((item: Item) => { + this.item = item; + }); + + this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; + this.url = this.router.url; + if (this.url.indexOf('?') > 0) { + this.url = this.url.substr(0, this.url.indexOf('?')); + } + this.hasChanges().pipe(first()).subscribe((hasChanges) => { + if (!hasChanges) { + this.initializeOriginalFields(); + } else { + this.checkLastModified(); + } + }); + + this.initializeNotificationsPrefix(); + } + + /** + * Initialize the prefix for notification messages + */ + abstract initializeNotificationsPrefix(): void; + + /** + * Sends all initial values of this item to the object updates service + */ + abstract initializeOriginalFields(): void; + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackUpdate(index, update: FieldUpdate) { + return update && update.field ? update.field.uuid : undefined; + } + + /** + * Checks whether or not there are currently updates for this item + */ + hasChanges(): Observable { + return this.objectUpdatesService.hasUpdates(this.url); + } + + /** + * Check if the current page is entirely valid + */ + protected isValid() { + return this.objectUpdatesService.isValidPage(this.url); + } + + /** + * Checks if the current item is still in sync with the version in the store + * If it's not, a notification is shown and the changes are removed + */ + private checkLastModified() { + const currentVersion = this.item.lastModified; + this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( + (updateVersion: Date) => { + if (updateVersion.getDate() !== currentVersion.getDate()) { + this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); + this.initializeOriginalFields(); + } + } + ); + } + + /** + * Submit the current changes + */ + abstract submit(): void; + + /** + * Request the object updates service to discard all current changes to this item + * Shows a notification to remind the user that they can undo this + */ + discard() { + const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); + this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); + } + + /** + * Request the object updates service to undo discarding all changes to this item + */ + reinstate() { + this.objectUpdatesService.reinstateFieldUpdates(this.url); + } + + /** + * Checks whether or not the item is currently reinstatable + */ + isReinstatable(): Observable { + return this.objectUpdatesService.isReinstatable(this.url); + } + + /** + * Get translated notification title + * @param key + */ + protected getNotificationTitle(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.title'); + } + + /** + * Get translated notification content + * @param key + */ + protected getNotificationContent(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.content'); + + } +} diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 6b3e05c818..7c9202c3b9 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -6,8 +6,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { cloneDeep } from 'lodash'; import { Observable } from 'rxjs'; import { - FieldUpdate, - FieldUpdates, Identifiable } from '../../../core/data/object-updates/object-updates.reducer'; import { first, map, switchMap, take, tap } from 'rxjs/operators'; @@ -20,6 +18,7 @@ import { RegistryService } from '../../../core/registry/registry.service'; import { MetadataField } from '../../../core/metadata/metadatafield.model'; import { MetadatumViewModel } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; @Component({ selector: 'ds-item-metadata', @@ -29,28 +28,7 @@ import { Metadata } from '../../../core/shared/metadata.utils'; /** * Component for displaying an item's metadata edit page */ -export class ItemMetadataComponent implements OnInit { - - /** - * The item to display the edit page for - */ - item: Item; - /** - * The current values and updates for all this item's metadata fields - */ - updates$: Observable; - /** - * The current url of this page - */ - url: string; - /** - * The time span for being able to undo discarding changes - */ - private discardTimeOut: number; - /** - * Prefix for this component's notification translate keys - */ - private notificationsPrefix = 'item.edit.metadata.notifications.'; +export class ItemMetadataComponent extends AbstractItemUpdateComponent { /** * Observable with a list of strings with all existing metadata field keys @@ -58,90 +36,54 @@ export class ItemMetadataComponent implements OnInit { metadataFields$: Observable; constructor( - private itemService: ItemDataService, - private objectUpdatesService: ObjectUpdatesService, - private router: Router, - private notificationsService: NotificationsService, - private translateService: TranslateService, + protected itemService: ItemDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected router: Router, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private route: ActivatedRoute, - private metadataFieldService: RegistryService, + protected route: ActivatedRoute, + protected metadataFieldService: RegistryService, ) { - + super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } /** * Set up and initialize all fields */ ngOnInit(): void { + super.ngOnInit(); this.metadataFields$ = this.findMetadataFields(); - this.route.parent.data.pipe(map((data) => data.item)) - .pipe( - first(), - map((data: RemoteData) => data.payload) - ).subscribe((item: Item) => { - this.item = item; - }); - - this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { - if (!hasChanges) { - this.initializeOriginalFields(); - } else { - this.checkLastModified(); - } - }); this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); } + /** + * Initialize the prefix for notification messages + */ + public initializeNotificationsPrefix(): void { + this.notificationsPrefix = 'item.edit.metadata.notifications.'; + } + /** * Sends a new add update for a field to the object updates service * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum */ add(metadata: MetadatumViewModel = new MetadatumViewModel()) { this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); - - } - - /** - * Request the object updates service to discard all current changes to this item - * Shows a notification to remind the user that they can undo this - */ - discard() { - const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); - this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); - } - - /** - * Request the object updates service to undo discarding all changes to this item - */ - reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.url); } /** * Sends all initial values of this item to the object updates service */ - private initializeOriginalFields() { + public initializeOriginalFields() { this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); } - /** - * Prevent unnecessary rerendering so fields don't lose focus - */ - trackUpdate(index, update: FieldUpdate) { - return update && update.field ? update.field.uuid : undefined; - } - /** * Requests all current metadata for this item and requests the item service to update the item * Makes sure the new version of the item is rendered on the page */ - submit() { + public submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable; @@ -167,60 +109,6 @@ export class ItemMetadataComponent implements OnInit { }); } - /** - * Checks whether or not there are currently updates for this item - */ - hasChanges(): Observable { - return this.objectUpdatesService.hasUpdates(this.url); - } - - /** - * Checks whether or not the item is currently reinstatable - */ - isReinstatable(): Observable { - return this.objectUpdatesService.isReinstatable(this.url); - } - - /** - * Checks if the current item is still in sync with the version in the store - * If it's not, a notification is shown and the changes are removed - */ - private checkLastModified() { - const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( - (updateVersion: Date) => { - if (updateVersion.getDate() !== currentVersion.getDate()) { - this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); - this.initializeOriginalFields(); - } - } - ); - } - - /** - * Check if the current page is entirely valid - */ - private isValid() { - return this.objectUpdatesService.isValidPage(this.url); - } - - /** - * Get translated notification title - * @param key - */ - private getNotificationTitle(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.title'); - } - - /** - * Get translated notification content - * @param key - */ - private getNotificationContent(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.content'); - - } - /** * Method to request all metadata fields and convert them to a list of strings */ diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index c95812b4bb..cfa3b8f415 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -19,7 +19,7 @@
{{label}}
-
; - /** - * The current url of this page - */ - url: string; - /** - * Prefix for this component's notification translate keys - */ - private notificationsPrefix = 'item.edit.metadata.notifications.'; +/** + * Component for displaying an item's relationships edit page + */ +export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { /** * The labels of all different relations within this item @@ -56,47 +37,20 @@ export class ItemRelationshipsComponent implements OnInit { * Resolved relationships and types together in one observable */ resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>; - /** - * The time span for being able to undo discarding changes - */ - private discardTimeOut: number; - - constructor(private route: ActivatedRoute, - private router: Router, - private translateService: TranslateService, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private objectUpdatesService: ObjectUpdatesService, - private notificationsService: NotificationsService, - private itemDataService: ItemDataService) { - } ngOnInit(): void { - this.route.parent.data.pipe(map((data) => data.item)) - .pipe( - first(), - map((data: RemoteData) => data.payload) - ).subscribe((item: Item) => { - this.item = item; - }); - this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { - if (!hasChanges) { - this.initializeOriginalFields(); - } else { - this.checkLastModified(); - } - }); + super.ngOnInit(); + this.updates$ = this.getRelationships().pipe( - relationsToItems(this.item.id, this.itemDataService), + relationsToItems(this.item.id, this.itemService), switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items)) ); this.initRelationshipObservables(); } + /** + * Initialize the item's relationship observables for easier access across the component + */ initRelationshipObservables() { const relationships$ = this.getRelationships(); @@ -119,72 +73,36 @@ export class ItemRelationshipsComponent implements OnInit { } /** - * Prevent unnecessary rerendering so fields don't lose focus + * Initialize the prefix for notification messages */ - trackUpdate(index, update: FieldUpdate) { - return update && update.field ? update.field.uuid : undefined; + public initializeNotificationsPrefix(): void { + this.notificationsPrefix = 'item.edit.relationships.notifications.'; } - /** - * Checks whether or not there are currently updates for this item - */ - hasChanges(): Observable { - return this.objectUpdatesService.hasUpdates(this.url); - } - - /** - * Checks whether or not the item is currently reinstatable - */ - isReinstatable(): Observable { - return this.objectUpdatesService.isReinstatable(this.url); - } - - discard(): void { - const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut }); - this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); - } - - /** - * Request the object updates service to undo discarding all changes to this item - */ - reinstate() { - this.objectUpdatesService.reinstateFieldUpdates(this.url); - } - - submit(): void { + public submit(): void { const updatedItems$ = this.getRelationships().pipe( first(), - relationsToItems(this.item.id, this.itemDataService), + relationsToItems(this.item.id, this.itemService), switchMap((items: Item[]) => this.objectUpdatesService.getUpdatedFields(this.url, items) as Observable) ); // TODO: Delete relationships } - private initializeOriginalFields() { + /** + * Sends all initial values of this item to the object updates service + */ + public initializeOriginalFields() { this.getRelationships().pipe( first(), - relationsToItems(this.item.id, this.itemDataService) + relationsToItems(this.item.id, this.itemService) ).subscribe((items: Item[]) => { this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); }); } /** - * Checks if the current item is still in sync with the version in the store - * If it's not, a notification is shown and the changes are removed + * Fetch all the relationships of the item */ - private checkLastModified() { - const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( - (updateVersion: Date) => { - if (updateVersion.getDate() !== currentVersion.getDate()) { - this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); - this.initializeOriginalFields(); - } - } - ); - } - public getRelationships(): Observable { return this.item.relationships.pipe( getSucceededRemoteData(), @@ -195,34 +113,25 @@ export class ItemRelationshipsComponent implements OnInit { ); } + /** + * Transform the item's relationships of a specific type into related items + * @param label The relationship type's label + */ public getRelatedItemsByLabel(label: string): Observable { return this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel(label), - relationsToItems(this.item.id, this.itemDataService) + relationsToItems(this.item.id, this.itemService) ); } + /** + * Get FieldUpdates for the relationships of a specific type + * @param label The relationship type's label + */ public getUpdatesByLabel(label: string): Observable { return this.getRelatedItemsByLabel(label).pipe( switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) ) } - /** - * Get translated notification title - * @param key - */ - private getNotificationTitle(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.title'); - } - - /** - * Get translated notification content - * @param key - */ - private getNotificationContent(key: string) { - return this.translateService.instant(this.notificationsPrefix + key + '.content'); - - } - } From a99fa4d4a26c1143e386cc0791d7ba93d3b1235a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Apr 2019 13:05:43 +0200 Subject: [PATCH 016/149] 61142: Renaming and refactoring item-relationships --- .../abstract-item-update.component.ts | 10 ++++++++- .../edit-item-page/edit-item-page.module.ts | 4 ++-- .../item-metadata/item-metadata.component.ts | 6 +++++ .../edit-relationship.component.html} | 0 .../edit-relationship.component.scss} | 0 .../edit-relationship.component.spec.ts} | 0 .../edit-relationship.component.ts} | 22 ++++++++++++++----- .../item-relationships.component.html | 2 +- .../item-relationships.component.ts | 18 ++++++++++----- 9 files changed, 47 insertions(+), 15 deletions(-) rename src/app/+item-page/edit-item-page/item-relationships/{edit-in-place-relationship/edit-in-place-relationship.component.html => edit-relationship/edit-relationship.component.html} (100%) rename src/app/+item-page/edit-item-page/item-relationships/{edit-in-place-relationship/edit-in-place-relationship.component.scss => edit-relationship/edit-relationship.component.scss} (100%) rename src/app/+item-page/edit-item-page/item-relationships/{edit-in-place-relationship/edit-in-place-relationship.component.spec.ts => edit-relationship/edit-relationship.component.spec.ts} (100%) rename src/app/+item-page/edit-item-page/item-relationships/{edit-in-place-relationship/edit-in-place-relationship.component.ts => edit-relationship/edit-relationship.component.ts} (74%) diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 3cc2a5ed84..76e6eb9446 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -24,7 +24,8 @@ export abstract class AbstractItemUpdateComponent implements OnInit { */ protected item: Item; /** - * The current values and updates for all this item's metadata fields + * The current values and updates for all this item's fields + * Should be initialized in the initializeUpdates method of the child component */ protected updates$: Observable; /** @@ -33,6 +34,7 @@ export abstract class AbstractItemUpdateComponent implements OnInit { protected url: string; /** * Prefix for this component's notification translate keys + * Should be initialized in the initializeNotificationsPrefix method of the child component */ protected notificationsPrefix; /** @@ -78,8 +80,14 @@ export abstract class AbstractItemUpdateComponent implements OnInit { }); this.initializeNotificationsPrefix(); + this.initializeUpdates(); } + /** + * Initialize the values and updates of the current item's fields + */ + abstract initializeUpdates(): void; + /** * Initialize the prefix for notification messages */ 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 079f065d5f..db7557b43c 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 @@ -16,7 +16,7 @@ import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; -import { EditInPlaceRelationshipComponent } from './item-relationships/edit-in-place-relationship/edit-in-place-relationship.component'; +import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -42,7 +42,7 @@ import { EditInPlaceRelationshipComponent } from './item-relationships/edit-in-p ItemRelationshipsComponent, ItemBitstreamsComponent, EditInPlaceFieldComponent, - EditInPlaceRelationshipComponent + EditRelationshipComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 7c9202c3b9..6e8be0efb6 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -54,6 +54,12 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { ngOnInit(): void { super.ngOnInit(); this.metadataFields$ = this.findMetadataFields(); + } + + /** + * Initialize the values and updates of the current item's metadata fields + */ + public initializeUpdates(): void { this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); } diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html similarity index 100% rename from src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.html rename to src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss similarity index 100% rename from src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.scss rename to src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts similarity index 100% rename from src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.spec.ts rename to src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts similarity index 74% rename from src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts rename to src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index bb29f7a889..b7ca15f211 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-in-place-relationship/edit-in-place-relationship.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -5,16 +5,14 @@ import { Item } from '../../../../core/shared/item.model'; import { VIEW_MODE_ELEMENT } from '../../../simple/related-items/related-items-component'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; -import { Observable } from 'rxjs/internal/Observable'; -import { map } from 'rxjs/operators'; @Component({ // tslint:disable-next-line:component-selector - selector: '[ds-edit-in-place-relationship]', - styleUrls: ['./edit-in-place-relationship.component.scss'], - templateUrl: './edit-in-place-relationship.component.html', + selector: '[ds-edit-relationship]', + styleUrls: ['./edit-relationship.component.scss'], + templateUrl: './edit-relationship.component.html', }) -export class EditInPlaceRelationshipComponent implements OnChanges { +export class EditRelationshipComponent implements OnChanges { /** * The current field, value and state of the relationship */ @@ -45,18 +43,30 @@ export class EditInPlaceRelationshipComponent implements OnChanges { this.item = cloneDeep(this.fieldUpdate.field) as Item; } + /** + * Sends a new remove update for this field to the object updates service + */ remove(): void { this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.item); } + /** + * Cancels the current update for this field in the object updates service + */ undo(): void { this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.item.uuid); } + /** + * Check if a user should be allowed to remove this field + */ canRemove(): boolean { return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; } + /** + * Check if a user should be allowed to cancel the update to this field + */ canUndo(): boolean { return this.fieldUpdate.changeType >= 0; } diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index cfa3b8f415..6d7f82a3da 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -20,7 +20,7 @@
{{label}}
-
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index 6d7f82a3da..50b64fed16 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -17,17 +17,34 @@  {{"item.edit.metadata.save-button" | translate}}
-
-
{{label}}
+
+
{{getRelationshipMessageKey(label) | translate}}
+ [ngClass]="{'alert alert-danger': updateValue.changeType === 2}"> +
+
+
+
+ + +
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss index 898533a9f0..cbedd42280 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss @@ -20,3 +20,14 @@ } + +.relationship-row:not(.alert-danger) { + padding: $alert-padding-y 0; +} + +.relationship-row.alert-danger { + margin-left: -$alert-padding-x; + margin-right: -$alert-padding-x; + margin-top: -1px; + margin-bottom: -1px; +} diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index adcf0abcad..56bac6c478 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -1,8 +1,8 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import { FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { switchMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, take } from 'rxjs/operators'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -98,4 +98,16 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { ) } + /** + * Get the i18n message key for a relationship + * @param label The relationship type's label + */ + public getRelationshipMessageKey(label: string): string { + if (label.indexOf('Of') > -1) { + return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` + } else { + return label; + } + } + } From 1e31fadb70e016008927cbb1f9c473870f88c6dd Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 4 Apr 2019 12:39:25 +0200 Subject: [PATCH 019/149] 61142: Submit intermediate commit --- resources/i18n/en.json | 1 + .../item-relationships.component.html | 23 +++++++----- .../item-relationships.component.ts | 35 ++++++++++++++++--- src/app/core/data/relationship.service.ts | 13 +++---- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index b64edd42d5..284fab6c82 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -733,6 +733,7 @@ "sub-communities": "Loading sub-communities...", "recent-submissions": "Loading recent submissions...", "item": "Loading item...", + "items": "Loading items...", "objects": "Loading...", "search-results": "Loading search results...", "browse-by": "Loading items...", diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index 50b64fed16..be400649c4 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -18,14 +18,21 @@
-
{{getRelationshipMessageKey(label) | translate}}
-
-
+ +
+
{{getRelationshipMessageKey(label) | translate}}
+ +
+
+ +
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 56bac6c478..963db2a67e 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -2,7 +2,8 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { distinctUntilChanged, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -11,6 +12,10 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { RelationshipService } from '../../../core/data/relationship.service'; +import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; +import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; +import { RestResponse } from '../../../core/cache/response.models'; +import { isNotEmptyOperator } from '../../../shared/empty.util'; @Component({ selector: 'ds-item-relationships', @@ -65,10 +70,32 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { } public submit(): void { - const updatedItems$ = this.relationshipService.getRelatedItems(this.item).pipe( - switchMap((items: Item[]) => this.objectUpdatesService.getUpdatedFields(this.url, items) as Observable) + const removedItemIds$ = this.relationshipService.getRelatedItems(this.item).pipe( + switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items) as Observable), + map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)), + map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field.uuid) as string[]), + isNotEmptyOperator() ); - // TODO: Delete relationships + const allRelationshipsAndRemovedItemIds$ = observableCombineLatest( + this.relationshipService.getItemRelationshipsArray(this.item), + removedItemIds$ + ); + const removedRelationshipIds$ = allRelationshipsAndRemovedItemIds$.pipe( + map(([relationships, itemIds]) => + relationships + .filter((relationship: Relationship) => itemIds.indexOf(relationship.leftId) > -1 || itemIds.indexOf(relationship.rightId) > -1) + .map((relationship: Relationship) => relationship.id)) + ); + removedRelationshipIds$.pipe( + take(1), + switchMap((removedIds: string[]) => observableZip(removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))), + map((responses: RestResponse[]) => responses.filter((response: RestResponse) => response.isSuccessful)) + ).subscribe((responses: RestResponse[]) => { + console.log(responses); + this.initializeOriginalFields(); + this.initializeUpdates(); + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + }); } /** diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 6e30696325..c6b8e8319c 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -3,7 +3,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, flatMap, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, @@ -46,17 +46,12 @@ export class RelationshipService { } deleteRelationship(uuid: string): Observable { - const requestUuid = this.requestService.generateRequestId(); - - this.getRelationshipEndpoint(uuid).pipe( + return this.getRelationshipEndpoint(uuid).pipe( isNotEmptyOperator(), distinctUntilChanged(), - map((endpointURL: string) => new DeleteRequest(requestUuid, endpointURL)), + map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), - take(1) - ).subscribe(); - - return this.requestService.getByUUID(requestUuid).pipe( + switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), filterSuccessfulResponses() ); } From bb683734894bfd6f79e57fd3bedaa966cb008f8a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 4 Apr 2019 15:19:28 +0200 Subject: [PATCH 020/149] 61142: Working submit (except reloading lists) and JSDocs --- .../item-relationships.component.ts | 12 +++++-- .../object-updates/object-updates.service.ts | 6 ++++ src/app/core/data/relationship.service.ts | 32 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 963db2a67e..90bee12187 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -69,7 +69,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { this.notificationsPrefix = 'item.edit.relationships.notifications.'; } + /** + * Resolve the currently selected related items back to relationships and send a delete request + * Make sure the lists are refreshed afterwards + */ public submit(): void { + // Get all IDs of related items of which their relationship with the current item is about to be removed const removedItemIds$ = this.relationshipService.getRelatedItems(this.item).pipe( switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items) as Observable), map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)), @@ -80,18 +85,21 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { this.relationshipService.getItemRelationshipsArray(this.item), removedItemIds$ ); + // Get all IDs of the relationships that should be removed const removedRelationshipIds$ = allRelationshipsAndRemovedItemIds$.pipe( map(([relationships, itemIds]) => relationships .filter((relationship: Relationship) => itemIds.indexOf(relationship.leftId) > -1 || itemIds.indexOf(relationship.rightId) > -1) .map((relationship: Relationship) => relationship.id)) ); + // Request a delete for every relationship found in the observable created above removedRelationshipIds$.pipe( take(1), - switchMap((removedIds: string[]) => observableZip(removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))), + switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))), map((responses: RestResponse[]) => responses.filter((response: RestResponse) => response.isSuccessful)) ).subscribe((responses: RestResponse[]) => { - console.log(responses); + // Make sure the lists are up-to-date and send a notification that the removal was successful + // TODO: Fix lists refreshing correctly this.initializeOriginalFields(); this.initializeUpdates(); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 6ef3ca91ca..c5c44fe36c 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -103,6 +103,12 @@ export class ObjectUpdatesService { })) } + /** + * Method that combines the state's updates (excluding updates that aren't part of the initialFields) with + * the initial values (when there's no update) to create a FieldUpdates object + * @param url The URL of the page for which the FieldUpdates should be requested + * @param initialFields The initial values of the fields + */ getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable { const objectUpdates = this.getObjectEntry(url); return objectUpdates.pipe(map((objectEntry) => { diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index c6b8e8319c..d0308dead7 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -39,12 +39,20 @@ export class RelationshipService { protected itemService: ItemDataService) { } + /** + * Get the endpoint for a relationship by ID + * @param uuid + */ getRelationshipEndpoint(uuid: string) { return this.halService.getEndpoint(this.linkPath).pipe( map((href: string) => `${href}/${uuid}`) ); } + /** + * Send a delete request for a relationship by ID + * @param uuid + */ deleteRelationship(uuid: string): Observable { return this.getRelationshipEndpoint(uuid).pipe( isNotEmptyOperator(), @@ -56,6 +64,11 @@ export class RelationshipService { ); } + /** + * Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types + * This is used for easier access of a relationship's type because they exist as observables + * @param item + */ getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> { const relationships$ = this.getItemRelationshipsArray(item); @@ -74,6 +87,10 @@ export class RelationshipService { ); } + /** + * Get an item their relationships in the form of an array + * @param item + */ getItemRelationshipsArray(item: Item): Observable { return item.relationships.pipe( getSucceededRemoteData(), @@ -84,6 +101,11 @@ export class RelationshipService { ); } + /** + * Get an array of an item their unique relationship type's labels + * The array doesn't contain any duplicate labels + * @param item + */ getItemRelationshipLabels(item: Item): Observable { return this.getItemResolvedRelsAndTypes(item).pipe( map(([relsCurrentPage, relTypesCurrentPage]) => { @@ -100,12 +122,22 @@ export class RelationshipService { ) } + /** + * Resolve a given item's relationships into related items and return the items as an array + * @param item + */ getRelatedItems(item: Item): Observable { return this.getItemRelationshipsArray(item).pipe( relationsToItems(item.uuid, this.itemService) ); } + /** + * Resolve a given item's relationships into related items, filtered by a relationship label + * and return the items as an array + * @param item + * @param label + */ getRelatedItemsByLabel(item: Item, label: string): Observable { return this.getItemResolvedRelsAndTypes(item).pipe( filterRelationsByTypeLabel(label), From 0cf4cdc1c5a80d249792fa06c53d99d88640f7dd Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 4 Apr 2019 17:43:40 +0200 Subject: [PATCH 021/149] 61142: ItemRelationshipComponent test + item de-caching --- .../item-metadata.component.spec.ts | 2 +- .../item-relationships.component.spec.ts | 234 ++++++++++++++++++ .../item-relationships.component.ts | 8 +- 3 files changed, 242 insertions(+), 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index f2cd74fc2f..2b50b75a2a 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -28,7 +28,7 @@ import { MetadataSchema } from '../../../core/metadata/metadataschema.model'; import { MetadataField } from '../../../core/metadata/metadatafield.model'; import { Metadata } from '../../../core/shared/metadata.utils'; -let comp: ItemMetadataComponent; +let comp: any; let fixture: ComponentFixture; let de: DebugElement; let el: HTMLElement; 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 e69de29bb2..8579d25fdf 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 @@ -0,0 +1,234 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ItemRelationshipsComponent } from './item-relationships.component'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { TestScheduler } from 'rxjs/testing'; +import { SharedModule } from '../../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { GLOBAL_CONFIG } from '../../../../config'; +import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; +import { RelationshipService } from '../../../core/data/relationship.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { getTestScheduler } from 'jasmine-marbles'; +import { By } from '@angular/platform-browser'; +import { RestResponse } from '../../../core/cache/response.models'; + +let comp: any; +let fixture: ComponentFixture; +let de: DebugElement; +let el: HTMLElement; +let objectUpdatesService; +let relationshipService; +let objectCache; +const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); +const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); +const successNotification: INotification = new Notification('id', NotificationType.Success, 'success'); +const notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } +); +const router = new RouterStub(); +let routeStub; +let itemService; + +const url = 'http://test-url.com/test-url'; +router.url = url; + +let scheduler: TestScheduler; +let item; +let author1; +let author2; +let fieldUpdate1; +let fieldUpdate2; +let relationships; +let relationshipType; + +describe('ItemRelationshipsComponent', () => { + beforeEach(async(() => { + const date = new Date(); + + relationshipType = Object.assign(new RelationshipType(), { + type: ResourceType.RelationshipType, + id: '1', + uuid: '1', + leftLabel: 'isAuthorOfPublication', + rightLabel: 'isPublicationOfAuthor' + }); + + relationships = [ + Object.assign(new Relationship(), { + self: url + '/2', + id: '2', + uuid: '2', + leftId: 'author1', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }), + Object.assign(new Relationship(), { + self: url + '/3', + id: '3', + uuid: '3', + leftId: 'author2', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }) + ]; + + item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))), + lastModified: date + }); + + author1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + author2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' + }); + + fieldUpdate1 = { + field: author1, + changeType: undefined + }; + fieldUpdate2 = { + field: author2, + changeType: FieldChangeType.REMOVE + }; + + itemService = jasmine.createSpyObj('itemService', { + findById: observableOf(new RemoteData(false, false, true, undefined, item)) + }); + routeStub = { + parent: { + data: observableOf({ item: new RemoteData(false, false, true, null, item) }) + } + }; + + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + getFieldUpdates: observableOf({ + [author1.uuid]: fieldUpdate1, + [author2.uuid]: fieldUpdate2 + }), + getFieldUpdatesExclusive: observableOf({ + [author1.uuid]: fieldUpdate1, + [author2.uuid]: fieldUpdate2 + }), + saveAddFieldUpdate: {}, + discardFieldUpdates: {}, + reinstateFieldUpdates: observableOf(true), + initialize: {}, + getUpdatedFields: observableOf([author1, author2]), + getLastModified: observableOf(date), + hasUpdates: observableOf(true), + isReinstatable: observableOf(false), // should always return something --> its in ngOnInit + isValidPage: observableOf(true) + } + ); + + relationshipService = jasmine.createSpyObj('relationshipService', + { + getItemRelationshipLabels: observableOf(['isAuthorOfPublication']), + getRelatedItems: observableOf([author1, author2]), + getRelatedItemsByLabel: observableOf([author1, author2]), + getItemRelationshipsArray: observableOf(relationships), + deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')) + } + ); + + objectCache = jasmine.createSpyObj('objectCache', { + remove: undefined + }); + + scheduler = getTestScheduler(); + TestBed.configureTestingModule({ + imports: [SharedModule, TranslateModule.forRoot()], + declarations: [ItemRelationshipsComponent], + providers: [ + { provide: ItemDataService, useValue: itemService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any }, + { provide: RelationshipService, useValue: relationshipService }, + { provide: ObjectCacheService, useValue: objectCache } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemRelationshipsComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + el = de.nativeElement; + comp.url = url; + fixture.detectChanges(); + }); + + describe('discard', () => { + beforeEach(() => { + comp.discard(); + }); + + it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => { + expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification); + }); + }); + + describe('reinstate', () => { + beforeEach(() => { + comp.reinstate(); + }); + + it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => { + expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url); + }); + }); + + describe('changeType is REMOVE', () => { + beforeEach(() => { + fieldUpdate1.changeType = FieldChangeType.REMOVE; + fixture.detectChanges(); + }); + it('the div should have class alert-danger', () => { + const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; + expect(element.classList).toContain('alert-danger'); + }); + }); + + describe('submit', () => { + beforeEach(() => { + comp.submit(); + }); + + it('it should delete the correct relationship and de-cache the current item', () => { + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid); + expect(objectCache.remove).toHaveBeenCalledWith(item.self); + }); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 90bee12187..bcf43bf364 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -16,6 +16,9 @@ import { FieldChangeType } from '../../../core/data/object-updates/object-update import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { RestResponse } from '../../../core/cache/response.models'; import { isNotEmptyOperator } from '../../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; @Component({ selector: 'ds-item-relationships', @@ -40,7 +43,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { protected translateService: TranslateService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected route: ActivatedRoute, - protected relationshipService: RelationshipService + protected relationshipService: RelationshipService, + protected objectCache: ObjectCacheService ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } @@ -100,6 +104,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { ).subscribe((responses: RestResponse[]) => { // Make sure the lists are up-to-date and send a notification that the removal was successful // TODO: Fix lists refreshing correctly + this.objectCache.remove(this.item.self); + this.itemService.findById(this.item.id).pipe(getSucceededRemoteData(), take(1)).subscribe((itemRD: RemoteData) => this.item = itemRD.payload); this.initializeOriginalFields(); this.initializeUpdates(); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); From b78b2e5b8227ebd828bc6861f99c04e6efec6b71 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 5 Apr 2019 13:46:48 +0200 Subject: [PATCH 022/149] 61142: EditRelationshipComponent tests + de-caching of requests in ItemRelationships --- .../edit-relationship.component.spec.ts | 180 ++++++++++++++++++ .../item-relationships.component.ts | 5 +- src/app/core/cache/object-cache.service.ts | 4 +- 3 files changed, 186 insertions(+), 3 deletions(-) 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 e69de29bb2..fc6c999a1c 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 @@ -0,0 +1,180 @@ +import { async, TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { TranslateModule } from '@ngx-translate/core'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { EditRelationshipComponent } from './edit-relationship.component'; +import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; +import { ResourceType } from '../../../../core/shared/resource-type'; +import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Item } from '../../../../core/shared/item.model'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; + +let objectUpdatesService: ObjectUpdatesService; +const url = 'http://test-url.com/test-url'; + +let item; +let author1; +let author2; +let fieldUpdate1; +let fieldUpdate2; +let relationships; +let relationshipType; + +let fixture; +let comp: EditRelationshipComponent; +let de; +let el; + +describe('EditRelationshipComponent', () => { + beforeEach(async(() => { + relationshipType = Object.assign(new RelationshipType(), { + type: ResourceType.RelationshipType, + id: '1', + uuid: '1', + leftLabel: 'isAuthorOfPublication', + rightLabel: 'isPublicationOfAuthor' + }); + + relationships = [ + Object.assign(new Relationship(), { + self: url + '/2', + id: '2', + uuid: '2', + leftId: 'author1', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }), + Object.assign(new Relationship(), { + self: url + '/3', + id: '3', + uuid: '3', + leftId: 'author2', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }) + ]; + + item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + }); + + author1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + author2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' + }); + + fieldUpdate1 = { + field: author1, + changeType: undefined + }; + fieldUpdate2 = { + field: author2, + changeType: FieldChangeType.REMOVE + }; + + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + saveChangeFieldUpdate: {}, + saveRemoveFieldUpdate: {}, + setEditableFieldUpdate: {}, + setValidFieldUpdate: {}, + removeSingleFieldUpdate: {}, + isEditable: observableOf(false), // should always return something --> its in ngOnInit + isValid: observableOf(true) // should always return something --> its in ngOnInit + } + ); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [EditRelationshipComponent], + providers: [ + { provide: ObjectUpdatesService, useValue: objectUpdatesService } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditRelationshipComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + el = de.nativeElement; + + comp.url = url; + comp.fieldUpdate = fieldUpdate1; + comp.item = item; + + fixture.detectChanges(); + }); + + describe('when fieldUpdate has no changeType', () => { + beforeEach(() => { + comp.fieldUpdate = fieldUpdate1; + fixture.detectChanges(); + }); + + describe('canRemove', () => { + it('should return true', () => { + expect(comp.canRemove()).toBe(true); + }); + }); + + describe('canUndo', () => { + it('should return false', () => { + expect(comp.canUndo()).toBe(false); + }); + }); + }); + + describe('when fieldUpdate has DELETE as changeType', () => { + beforeEach(() => { + comp.fieldUpdate = fieldUpdate2; + fixture.detectChanges(); + }); + + describe('canRemove', () => { + it('should return false', () => { + expect(comp.canRemove()).toBe(false); + }); + }); + + describe('canUndo', () => { + it('should return true', () => { + expect(comp.canUndo()).toBe(true); + }); + }); + }); + + describe('remove', () => { + beforeEach(() => { + comp.remove(); + }); + + it('should call saveRemoveFieldUpdate with the correct arguments', () => { + expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, item); + }); + }); + + describe('undo', () => { + beforeEach(() => { + comp.undo(); + }); + + it('should call removeSingleFieldUpdate with the correct arguments', () => { + expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, item.uuid); + }); + }); + +}); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index bcf43bf364..9d348c6e13 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -19,6 +19,7 @@ import { isNotEmptyOperator } from '../../../shared/empty.util'; import { RemoteData } from '../../../core/data/remote-data'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { RequestService } from '../../../core/data/request.service'; @Component({ selector: 'ds-item-relationships', @@ -44,7 +45,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected route: ActivatedRoute, protected relationshipService: RelationshipService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected requestService: RequestService ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } @@ -105,6 +107,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { // Make sure the lists are up-to-date and send a notification that the removal was successful // TODO: Fix lists refreshing correctly this.objectCache.remove(this.item.self); + this.requestService.removeByHrefSubstring(this.item.self); this.itemService.findById(this.item.id).pipe(getSucceededRemoteData(), take(1)).subscribe((itemRD: RemoteData) => this.item = itemRD.payload); this.initializeOriginalFields(); this.initializeUpdates(); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 483de65b98..d415da50b3 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -68,8 +68,8 @@ export class ObjectCacheService { * @param href * The unique href of the object to be removed */ - remove(uuid: string): void { - this.store.dispatch(new RemoveFromObjectCacheAction(uuid)); + remove(href: string): void { + this.store.dispatch(new RemoveFromObjectCacheAction(href)); } /** From 9f27a89dc4398b80303cd4ffc5d4f4ba0974aae8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 5 Apr 2019 15:21:10 +0200 Subject: [PATCH 023/149] 61142: RelationshipService tests --- .../core/data/relationship.service.spec.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/app/core/data/relationship.service.spec.ts diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts new file mode 100644 index 0000000000..0e417b7ffe --- /dev/null +++ b/src/app/core/data/relationship.service.spec.ts @@ -0,0 +1,137 @@ +import { RelationshipService } from './relationship.service'; +import { RequestService } from './request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +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'; +import { Item } from '../shared/item.model'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { DeleteRequest } from './request.models'; + +describe('RelationshipService', () => { + let service: RelationshipService; + let requestService: RequestService; + + const restEndpointURL = 'https://rest.api/'; + const relationshipsEndpointURL = `${restEndpointURL}/relationships`; + const halService: any = new HALEndpointServiceStub(restEndpointURL); + const rdbService = getMockRemoteDataBuildService(); + + const relationshipType = Object.assign(new RelationshipType(), { + type: ResourceType.RelationshipType, + id: '1', + uuid: '1', + leftLabel: 'isAuthorOfPublication', + rightLabel: 'isPublicationOfAuthor' + }); + + const relationships = [ + Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/2', + id: '2', + uuid: '2', + leftId: 'author1', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }), + Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/3', + id: '3', + uuid: '3', + leftId: 'author2', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }) + ]; + + const item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + }); + + const relatedItem1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + const relatedItem2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' + }); + const relatedItems = [relatedItem1, relatedItem2]; + + const itemService = jasmine.createSpyObj('itemService', { + findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((item) => item.id === uuid)[0]) + }); + + function initTestService() { + return new RelationshipService( + requestService, + halService, + rdbService, + itemService + ); + } + + const getRequestEntry$ = (successful: boolean) => { + return observableOf({ + response: { isSuccessful: successful, payload: relationships } as any + } as RequestEntry) + }; + + beforeEach(() => { + requestService = getMockRequestService(getRequestEntry$(true)); + service = initTestService(); + }); + + describe('deleteRelationship', () => { + beforeEach(() => { + service.deleteRelationship(relationships[0].uuid).subscribe(); + }); + + it('should send a DeleteRequest', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationships[0].uuid); + expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); + }); + }); + + describe('getItemRelationshipsArray', () => { + it('should return the item\'s relationships in the form of an array', () => { + service.getItemRelationshipsArray(item).subscribe((result) => { + expect(result).toEqual(relationships); + }); + }); + }); + + describe('getItemRelationshipLabels', () => { + it('should return the correct labels', () => { + service.getItemRelationshipLabels(item).subscribe((result) => { + expect(result).toEqual([relationshipType.rightLabel]); + }); + }); + }); + + describe('getRelatedItems', () => { + it('should return the related items', () => { + service.getRelatedItems(item).subscribe((result) => { + expect(result).toEqual(relatedItems); + }); + }); + }); + + describe('getRelatedItemsByLabel', () => { + it('should return the related items by label', () => { + service.getRelatedItemsByLabel(item, relationshipType.rightLabel).subscribe((result) => { + expect(result).toEqual(relatedItems); + }); + }); + }) + +}); From 01b60dbf3459a6542bdfeb36ad5118d135117f36 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 5 Apr 2019 17:28:20 +0200 Subject: [PATCH 024/149] 61142: RemoveByHrefSubstring fix --- .../item-relationships/item-relationships.component.ts | 2 +- src/app/core/data/request.service.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 9d348c6e13..8e8a31f896 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -108,7 +108,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { // TODO: Fix lists refreshing correctly this.objectCache.remove(this.item.self); this.requestService.removeByHrefSubstring(this.item.self); - this.itemService.findById(this.item.id).pipe(getSucceededRemoteData(), take(1)).subscribe((itemRD: RemoteData) => this.item = itemRD.payload); + // this.itemService.findById(this.item.id).pipe(getSucceededRemoteData(), take(1)).subscribe((itemRD: RemoteData) => this.item = itemRD.payload); this.initializeOriginalFields(); this.initializeUpdates(); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index fd463047f1..82c2a47491 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { Observable, race as observableRace } from 'rxjs'; -import { filter, mergeMap, take } from 'rxjs/operators'; +import { filter, map, mergeMap, take, tap } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -64,8 +64,7 @@ const uuidsFromHrefSubstringSelector = const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => { let result = []; if (isNotEmpty(state)) { - result = Object.values(state) - .filter((value: string) => value.startsWith(href)); + result = Object.keys(state).filter((key) => key.startsWith(href)).map((key) => state[key]); } return result; }; From bbbd6959a8b01f02707cbb2520a3b649e0e4d7ec Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Apr 2019 15:28:18 +0200 Subject: [PATCH 025/149] 61142: EditRelationshipList component to proper reload relationships and fix performance issues --- .../edit-item-page/edit-item-page.module.ts | 4 +- .../edit-relationship-list.component.html | 15 ++ .../edit-relationship-list.component.scss | 12 ++ .../edit-relationship-list.component.spec.ts | 137 ++++++++++++++++++ .../edit-relationship-list.component.ts | 99 +++++++++++++ .../item-relationships.component.html | 16 +- .../item-relationships.component.scss | 11 -- .../item-relationships.component.spec.ts | 28 ++-- .../item-relationships.component.ts | 60 ++++---- src/app/core/cache/object-cache.service.ts | 14 +- src/app/core/data/request.service.ts | 11 ++ 11 files changed, 332 insertions(+), 75 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts create mode 100644 src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts 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 db7557b43c..1542d12ce5 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 @@ -17,6 +17,7 @@ import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/e import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component'; +import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -42,7 +43,8 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi ItemRelationshipsComponent, ItemBitstreamsComponent, EditInPlaceFieldComponent, - EditRelationshipComponent + EditRelationshipComponent, + EditRelationshipListComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html new file mode 100644 index 0000000000..ba5164e81a --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html @@ -0,0 +1,15 @@ + +
+
{{getRelationshipMessageKey(relationshipLabel) | translate}}
+ +
+
+ +
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss new file mode 100644 index 0000000000..ec6cd2ba78 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss @@ -0,0 +1,12 @@ +@import '../../../../../styles/variables.scss'; + +.relationship-row:not(.alert-danger) { + padding: $alert-padding-y 0; +} + +.relationship-row.alert-danger { + margin-left: -$alert-padding-x; + margin-right: -$alert-padding-x; + margin-top: -1px; + margin-bottom: -1px; +} 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 new file mode 100644 index 0000000000..bd5f5f2e5c --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -0,0 +1,137 @@ +import { EditRelationshipListComponent } from './edit-relationship-list.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; +import { ResourceType } from '../../../../core/shared/resource-type'; +import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Item } from '../../../../core/shared/item.model'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; +import { SharedModule } from '../../../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { RelationshipService } from '../../../../core/data/relationship.service'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +let comp: EditRelationshipListComponent; +let fixture: ComponentFixture; +let de: DebugElement; + +let objectUpdatesService; +let relationshipService; + +const url = 'http://test-url.com/test-url'; + +let item; +let author1; +let author2; +let fieldUpdate1; +let fieldUpdate2; +let relationships; +let relationshipType; + +describe('EditRelationshipListComponent', () => { + beforeEach(async(() => { + relationshipType = Object.assign(new RelationshipType(), { + type: ResourceType.RelationshipType, + id: '1', + uuid: '1', + leftLabel: 'isAuthorOfPublication', + rightLabel: 'isPublicationOfAuthor' + }); + + relationships = [ + Object.assign(new Relationship(), { + self: url + '/2', + id: '2', + uuid: '2', + leftId: 'author1', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }), + Object.assign(new Relationship(), { + self: url + '/3', + id: '3', + uuid: '3', + leftId: 'author2', + rightId: 'publication', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }) + ]; + + item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + }); + + author1 = Object.assign(new Item(), { + id: 'author1', + uuid: 'author1' + }); + author2 = Object.assign(new Item(), { + id: 'author2', + uuid: 'author2' + }); + + fieldUpdate1 = { + field: author1, + changeType: undefined + }; + fieldUpdate2 = { + field: author2, + changeType: FieldChangeType.REMOVE + }; + + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + getFieldUpdatesExclusive: observableOf({ + [author1.uuid]: fieldUpdate1, + [author2.uuid]: fieldUpdate2 + }) + } + ); + + relationshipService = jasmine.createSpyObj('relationshipService', + { + getRelatedItemsByLabel: observableOf([author1, author2]), + } + ); + + TestBed.configureTestingModule({ + imports: [SharedModule, TranslateModule.forRoot()], + declarations: [EditRelationshipListComponent], + providers: [ + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: RelationshipService, useValue: relationshipService } + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditRelationshipListComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + comp.item = item; + comp.url = url; + comp.relationshipLabel = relationshipType.leftLabel; + fixture.detectChanges(); + }); + + describe('changeType is REMOVE', () => { + beforeEach(() => { + fieldUpdate1.changeType = FieldChangeType.REMOVE; + fixture.detectChanges(); + }); + it('the div should have class alert-danger', () => { + const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; + expect(element.classList).toContain('alert-danger'); + }); + }); +}); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts new file mode 100644 index 0000000000..765d6484d4 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -0,0 +1,99 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer'; +import { RelationshipService } from '../../../../core/data/relationship.service'; +import { Item } from '../../../../core/shared/item.model'; +import { switchMap } from 'rxjs/operators'; +import { hasValue } from '../../../../shared/empty.util'; + +@Component({ + selector: 'ds-edit-relationship-list', + styleUrls: ['./edit-relationship-list.component.scss'], + templateUrl: './edit-relationship-list.component.html', +}) +/** + * A component creating a list of editable relationships of a certain type + * The relationships are rendered as a list of related items + */ +export class EditRelationshipListComponent implements OnInit, OnChanges { + /** + * The item to display related items for + */ + @Input() item: Item; + + /** + * The URL to the current page + * Used to fetch updates for the current item from the store + */ + @Input() url: string; + + /** + * The label of the relationship-type we're rendering a list for + */ + @Input() relationshipLabel: string; + + /** + * The FieldUpdates for the relationships in question + */ + updates$: Observable; + + constructor( + protected objectUpdatesService: ObjectUpdatesService, + protected relationshipService: RelationshipService + ) { + } + + ngOnInit(): void { + this.initUpdates(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.initUpdates(); + } + + /** + * Initialize the FieldUpdates using the related items + */ + initUpdates() { + this.updates$ = this.getUpdatesByLabel(this.relationshipLabel); + } + + /** + * Transform the item's relationships of a specific type into related items + * @param label The relationship type's label + */ + public getRelatedItemsByLabel(label: string): Observable { + return this.relationshipService.getRelatedItemsByLabel(this.item, label); + } + + /** + * Get FieldUpdates for the relationships of a specific type + * @param label The relationship type's label + */ + public getUpdatesByLabel(label: string): Observable { + return this.getRelatedItemsByLabel(label).pipe( + switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) + ) + } + + /** + * Get the i18n message key for a relationship + * @param label The relationship type's label + */ + public getRelationshipMessageKey(label: string): string { + if (hasValue(label) && label.indexOf('Of') > -1) { + return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` + } else { + return label; + } + } + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackUpdate(index, update: FieldUpdate) { + return update && update.field ? update.field.uuid : undefined; + } + +} diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index be400649c4..4bd0b3df2c 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -18,21 +18,7 @@
- -
-
{{getRelationshipMessageKey(label) | translate}}
- -
-
- -
-
-
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss index cbedd42280..898533a9f0 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss @@ -20,14 +20,3 @@ } - -.relationship-row:not(.alert-danger) { - padding: $alert-padding-y 0; -} - -.relationship-row.alert-danger { - margin-left: -$alert-padding-x; - margin-right: -$alert-padding-x; - margin-top: -1px; - margin-bottom: -1px; -} 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 8579d25fdf..2439eb4c63 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 @@ -1,6 +1,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ItemRelationshipsComponent } from './item-relationships.component'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { RouterStub } from '../../../shared/testing/router-stub'; @@ -24,8 +24,8 @@ import { FieldChangeType } from '../../../core/data/object-updates/object-update import { RelationshipService } from '../../../core/data/relationship.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getTestScheduler } from 'jasmine-marbles'; -import { By } from '@angular/platform-browser'; import { RestResponse } from '../../../core/cache/response.models'; +import { RequestService } from '../../../core/data/request.service'; let comp: any; let fixture: ComponentFixture; @@ -33,6 +33,7 @@ let de: DebugElement; let el: HTMLElement; let objectUpdatesService; let relationshipService; +let requestService; let objectCache; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -158,6 +159,13 @@ describe('ItemRelationshipsComponent', () => { } ); + requestService = jasmine.createSpyObj('requestService', + { + removeByHrefSubstring: {}, + hasByHrefObservable: observableOf(false) + } + ); + objectCache = jasmine.createSpyObj('objectCache', { remove: undefined }); @@ -174,7 +182,9 @@ describe('ItemRelationshipsComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any }, { provide: RelationshipService, useValue: relationshipService }, - { provide: ObjectCacheService, useValue: objectCache } + { provide: ObjectCacheService, useValue: objectCache }, + { provide: RequestService, useValue: requestService }, + ChangeDetectorRef ], schemas: [ NO_ERRORS_SCHEMA ] @@ -210,17 +220,6 @@ describe('ItemRelationshipsComponent', () => { }); }); - describe('changeType is REMOVE', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); - it('the div should have class alert-danger', () => { - const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; - expect(element.classList).toContain('alert-danger'); - }); - }); - describe('submit', () => { beforeEach(() => { comp.submit(); @@ -229,6 +228,7 @@ describe('ItemRelationshipsComponent', () => { it('it should delete the correct relationship and de-cache the current item', () => { expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid); expect(objectCache.remove).toHaveBeenCalledWith(item.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); }); }); }); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 8e8a31f896..7db8756c78 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -1,8 +1,8 @@ -import { Component, Inject } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { map, switchMap, take, tap } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -20,6 +20,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; +import { Subscription } from 'rxjs/internal/Subscription'; @Component({ selector: 'ds-item-relationships', @@ -29,13 +30,19 @@ import { RequestService } from '../../../core/data/request.service'; /** * Component for displaying an item's relationships edit page */ -export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { +export class ItemRelationshipsComponent extends AbstractItemUpdateComponent implements OnDestroy { /** * The labels of all different relations within this item */ relationLabels$: Observable; + /** + * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request + * This is used to update the item in cache after relationships are deleted + */ + itemUpdateSubscription: Subscription; + constructor( protected itemService: ItemDataService, protected objectUpdatesService: ObjectUpdatesService, @@ -46,7 +53,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { protected route: ActivatedRoute, protected relationshipService: RelationshipService, protected objectCache: ObjectCacheService, - protected requestService: RequestService + protected requestService: RequestService, + protected cdRef: ChangeDetectorRef ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } @@ -57,6 +65,16 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { ngOnInit(): void { super.ngOnInit(); this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item); + + // Update the item (and view) when it's removed in the request cache + this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( + filter((exists: boolean) => !exists), + switchMap(() => this.itemService.findById(this.item.uuid)), + getSucceededRemoteData(), + ).subscribe((itemRD: RemoteData) => { + this.item = itemRD.payload; + this.cdRef.detectChanges(); + }); } /** @@ -104,13 +122,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))), map((responses: RestResponse[]) => responses.filter((response: RestResponse) => response.isSuccessful)) ).subscribe((responses: RestResponse[]) => { - // Make sure the lists are up-to-date and send a notification that the removal was successful - // TODO: Fix lists refreshing correctly + // Remove the item's cache to make sure the lists are reloaded with the newest values this.objectCache.remove(this.item.self); this.requestService.removeByHrefSubstring(this.item.self); - // this.itemService.findById(this.item.id).pipe(getSucceededRemoteData(), take(1)).subscribe((itemRD: RemoteData) => this.item = itemRD.payload); this.initializeOriginalFields(); this.initializeUpdates(); + // Send a notification that the removal was successful this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); }); } @@ -125,33 +142,10 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { } /** - * Transform the item's relationships of a specific type into related items - * @param label The relationship type's label + * Unsubscribe from the item update when the component is destroyed */ - public getRelatedItemsByLabel(label: string): Observable { - return this.relationshipService.getRelatedItemsByLabel(this.item, label); - } - - /** - * Get FieldUpdates for the relationships of a specific type - * @param label The relationship type's label - */ - public getUpdatesByLabel(label: string): Observable { - return this.getRelatedItemsByLabel(label).pipe( - switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) - ) - } - - /** - * Get the i18n message key for a relationship - * @param label The relationship type's label - */ - public getRelationshipMessageKey(label: string): string { - if (label.indexOf('Of') > -1) { - return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` - } else { - return label; - } + ngOnDestroy(): void { + this.itemUpdateSubscription.unsubscribe(); } } diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d415da50b3..c1a78225b8 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -3,7 +3,7 @@ import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { applyPatch, Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, take, tap, } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; @@ -224,6 +224,18 @@ export class ObjectCacheService { return result; } + /** + * Create an observable that emits a new value whenever the availability of the cached object changes. + * The value it emits is a boolean stating if the object exists in cache or not. + * @param selfLink The self link of the object to observe + */ + hasBySelfLinkObservable(selfLink: string): Observable { + return this.store.pipe( + select(entryFromSelfLinkSelector(selfLink)), + map((entry: ObjectCacheEntry) => this.isValid(entry)) + ); + } + /** * Check whether an ObjectCacheEntry should still be cached * diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 82c2a47491..f15a60d5ec 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -265,4 +265,15 @@ export class RequestService { return result; } + /** + * Create an observable that emits a new value whenever the availability of the cached request changes. + * The value it emits is a boolean stating if the request exists in cache or not. + * @param href The href of the request to observe + */ + hasByHrefObservable(href: string): Observable { + return this.getByHref(href).pipe( + map((requestEntry: RequestEntry) => this.isValid(requestEntry)) + ); + } + } From eeb2c790c186da27d8254264b6658b97c8fad30a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Apr 2019 16:16:55 +0200 Subject: [PATCH 026/149] 61142: Fixed AoT build errors --- .../abstract-item-update.component.ts | 17 +++++++---------- .../item-metadata/item-metadata.component.ts | 14 +++++++++----- src/app/core/data/relationship.service.spec.ts | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 76e6eb9446..c49def3dd2 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, Injectable, OnInit } from '@angular/core'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; import { Item } from '../../../core/shared/item.model'; @@ -11,10 +11,7 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { first, map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; -@Component({ - selector: 'ds-abstract-item-update', - template: ``, -}) +@Injectable() /** * Abstract component for managing object updates of an item */ @@ -22,25 +19,25 @@ export abstract class AbstractItemUpdateComponent implements OnInit { /** * The item to display the edit page for */ - protected item: Item; + item: Item; /** * The current values and updates for all this item's fields * Should be initialized in the initializeUpdates method of the child component */ - protected updates$: Observable; + updates$: Observable; /** * The current url of this page */ - protected url: string; + url: string; /** * Prefix for this component's notification translate keys * Should be initialized in the initializeNotificationsPrefix method of the child component */ - protected notificationsPrefix; + notificationsPrefix; /** * The time span for being able to undo discarding changes */ - protected discardTimeOut: number; + discardTimeOut: number; constructor( protected itemService: ItemDataService, diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 6e8be0efb6..dbbcebfd00 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -60,7 +60,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * Initialize the values and updates of the current item's metadata fields */ public initializeUpdates(): void { - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships()); } /** @@ -82,7 +82,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); + this.objectUpdatesService.initialize(this.url, this.getMetadataAsListExcludingRelationships(), this.item.lastModified); } /** @@ -92,7 +92,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { public submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { - const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable; + const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.getMetadataAsListExcludingRelationships()) as Observable; metadata$.pipe( first(), switchMap((metadata: MetadatumViewModel[]) => { @@ -105,7 +105,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { (rd: RemoteData) => { this.item = rd.payload; this.initializeOriginalFields(); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships()); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); } ) @@ -124,4 +124,8 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { take(1), map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString()))); } + + getMetadataAsListExcludingRelationships(): MetadatumViewModel[] { + return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.')); + } } diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 0e417b7ffe..ce2b169eef 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -68,7 +68,7 @@ describe('RelationshipService', () => { const relatedItems = [relatedItem1, relatedItem2]; const itemService = jasmine.createSpyObj('itemService', { - findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((item) => item.id === uuid)[0]) + findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0]) }); function initTestService() { From dacf8136764c5070ee59bc8bd6c15c2dce9200f6 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 18 Apr 2019 17:24:24 +0200 Subject: [PATCH 027/149] 61142: Notifications on failed relationship delete requests --- resources/i18n/en.json | 3 ++ .../item-relationships.component.ts | 29 ++++++++++++------- src/app/core/data/relationship.service.ts | 4 +-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 284fab6c82..eb6f600e21 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -296,6 +296,9 @@ "saved": { "title": "Relationships saved", "content": "Your changes to this item's relationships were saved." + }, + "failed": { + "title": "Error deleting relationship" } } } diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 7db8756c78..3e74794866 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -14,7 +14,7 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { RelationshipService } from '../../../core/data/relationship.service'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { RestResponse } from '../../../core/cache/response.models'; +import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; import { isNotEmptyOperator } from '../../../shared/empty.util'; import { RemoteData } from '../../../core/data/remote-data'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -95,7 +95,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl /** * Resolve the currently selected related items back to relationships and send a delete request - * Make sure the lists are refreshed afterwards + * Make sure the lists are refreshed afterwards and notifications are sent for success and errors */ public submit(): void { // Get all IDs of related items of which their relationship with the current item is about to be removed @@ -119,16 +119,25 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl // Request a delete for every relationship found in the observable created above removedRelationshipIds$.pipe( take(1), - switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))), - map((responses: RestResponse[]) => responses.filter((response: RestResponse) => response.isSuccessful)) + switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))) ).subscribe((responses: RestResponse[]) => { - // Remove the item's cache to make sure the lists are reloaded with the newest values - this.objectCache.remove(this.item.self); - this.requestService.removeByHrefSubstring(this.item.self); + const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); + const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful); + + // Display an error notification for each failed request + failedResponses.forEach((response: ErrorResponse) => { + this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); + }); + if (successfulResponses.length > 0) { + // Remove the item's cache to make sure the lists are reloaded with the newest values + this.objectCache.remove(this.item.self); + this.requestService.removeByHrefSubstring(this.item.self); + // Send a notification that the removal was successful + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + } + // Reset the state of editing relationships this.initializeOriginalFields(); this.initializeUpdates(); - // Send a notification that the removal was successful - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); }); } diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index d0308dead7..ba77fb4f5a 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -7,7 +7,7 @@ import { distinctUntilChanged, flatMap, map, switchMap, take, tap } from 'rxjs/o import { configureRequest, filterSuccessfulResponses, - getRemoteDataPayload, + getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { DeleteRequest, RestRequest } from './request.models'; @@ -60,7 +60,7 @@ export class RelationshipService { map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), - filterSuccessfulResponses() + getResponseFromEntry() ); } From 5a06c9195e34b62ff734fefdf3bae89f44cd8da3 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 18 Apr 2019 17:42:38 +0200 Subject: [PATCH 028/149] 61142: Author seperator in publication list elements --- .../publication/publication-list-element.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html index aff19aec1d..d467edfb21 100644 --- a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html @@ -12,6 +12,7 @@ class="item-list-authors"> + ; From 15ed0cc8fa5ce4ff87403e5d2eee3cb8902fd4ee Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 23 Apr 2019 14:32:11 +0200 Subject: [PATCH 029/149] 61947: Thumbnail display fix --- src/app/core/shared/item.model.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 645b50d5db..7bd69131c6 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -8,6 +8,7 @@ import { Bitstream } from './bitstream.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { PaginatedList } from '../data/paginated-list'; import { Relationship } from './item-relationships/relationship.model'; +import { getSucceededRemoteData } from './operators'; export class Item extends DSpaceObject { @@ -95,7 +96,7 @@ export class Item extends DSpaceObject { */ getBitstreamsByBundleName(bundleName: string): Observable { return this.bitstreams.pipe( - filter((rd: RemoteData>) => !rd.isResponsePending), + getSucceededRemoteData(), map((rd: RemoteData>) => rd.payload.page), filter((bitstreams: Bitstream[]) => hasValue(bitstreams)), take(1), From 395a78c360aa927b515ebf61d2f80083340cac4e Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 28 May 2019 13:14:59 +0200 Subject: [PATCH 030/149] 62571: Update item move method --- .../item-move/item-move.component.ts | 5 ++++- src/app/core/data/item-data.service.ts | 22 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 7eb0e4c10e..4f6f9bbe08 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -17,6 +17,7 @@ import {getItemEditPath} from '../../item-page-routing.module'; import {Observable} from 'rxjs'; import {of as observableOf} from 'rxjs'; import { RestResponse } from '../../../core/cache/response.models'; +import { Collection } from '../../../core/shared/collection.model'; @Component({ selector: 'ds-item-move', @@ -34,6 +35,7 @@ export class ItemMoveComponent implements OnInit { itemRD$: Observable>; collectionSearchResults: Observable = observableOf([]); selectedCollection: string; + selectedCollectionObject: Collection; selectedCollectionId: string; itemId: string; @@ -92,6 +94,7 @@ export class ItemMoveComponent implements OnInit { onClick(data: any): void { this.selectedCollection = data.name; this.selectedCollectionId = data.id; + this.selectedCollectionObject = data; } /** @@ -105,7 +108,7 @@ export class ItemMoveComponent implements OnInit { * Moves the item to a new collection based on the selected collection */ moveCollection() { - this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionId).pipe(first()).subscribe( + this.itemDataService.moveToCollection(this.itemId, this.selectedCollectionObject).pipe(first()).subscribe( (response: RestResponse) => { this.router.navigate([getItemEditPath(this.itemId)]); if (response.isSuccessful) { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 1e6f3e50de..9d686d98d1 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -12,15 +12,17 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { DeleteByIDRequest, FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models'; +import { FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { configureRequest, getRequestFromRequestHref } from '../shared/operators'; import { RequestEntry } from './request.reducer'; import { RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Collection } from '../shared/collection.model'; @Injectable() export class ItemDataService extends DataService { @@ -120,22 +122,26 @@ export class ItemDataService extends DataService { ); } - public getMoveItemEndpoint(itemId: string, collectionId?: string): Observable { + public getMoveItemEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, itemId)), - map((endpoint: string) => `${endpoint}/owningCollection/move/${collectionId ? `/${collectionId}` : ''}`) + map((endpoint: string) => `${endpoint}/owningCollection`) ); } - public moveToCollection(itemId: string, collectionId: string): Observable { - const requestId = this.requestService.generateRequestId(); + public moveToCollection(itemId: string, collection: Collection): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; - const hrefObs = this.getMoveItemEndpoint(itemId, collectionId); + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getMoveItemEndpoint(itemId); hrefObs.pipe( find((href: string) => hasValue(href)), map((href: string) => { - const request = new PutRequest(requestId, href); + const request = new PutRequest(requestId, href, collection.self, options); this.requestService.configure(request); }) ).subscribe(); From 74bf69f984688a8195e4a5f3b19472e537843763 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 28 May 2019 15:47:42 +0200 Subject: [PATCH 031/149] 62571: Move item update and test fixes --- .../edit-item-page/edit-item-page.module.ts | 2 + .../item-move/item-move.component.spec.ts | 78 ++++++++++--------- .../item-move/item-move.component.ts | 8 +- 3 files changed, 47 insertions(+), 41 deletions(-) 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 e2f63ac5fc..de672c9ea7 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 @@ -15,6 +15,7 @@ import { ItemMetadataComponent } from './item-metadata/item-metadata.component'; import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemMoveComponent } from './item-move/item-move.component'; +import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; /** * Module that contains all components related to the Edit Item page administrator functionality @@ -23,6 +24,7 @@ import { ItemMoveComponent } from './item-move/item-move.component'; imports: [ CommonModule, SharedModule, + EditItemPageRoutingModule ], declarations: [ EditItemPageComponent, diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index 8f95441bad..3d947fdabe 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -1,22 +1,23 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {CommonModule} from '@angular/common'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ItemMoveComponent} from './item-move.component'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {SearchService} from '../../../+search-page/search-service/search.service'; -import {of as observableOf} from 'rxjs'; -import {FormsModule} from '@angular/forms'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {RemoteData} from '../../../core/data/remote-data'; -import {PaginatedList} from '../../../core/data/paginated-list'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemMoveComponent } from './item-move.component'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { SearchService } from '../../../+search-page/search-service/search.service'; +import { of as observableOf } from 'rxjs'; +import { FormsModule } from '@angular/forms'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RestResponse } from '../../../core/cache/response.models'; +import { Collection } from '../../../core/shared/collection.model'; describe('ItemMoveComponent', () => { let comp: ItemMoveComponent; @@ -49,20 +50,28 @@ describe('ItemMoveComponent', () => { }) }; + const collection1 = Object.assign(new Collection(),{ + uuid: 'collection-uuid-1', + name: 'Test collection 1', + self: 'self-link-1', + }); + + const collection2 = Object.assign(new Collection(),{ + uuid: 'collection-uuid-2', + name: 'Test collection 2', + self: 'self-link-2', + }); + const mockSearchService = { search: () => { return observableOf(new RemoteData(false, false, true, null, new PaginatedList(null, [ { - dspaceObject: { - name: 'Test collection 1', - uuid: 'collection1' - }, hitHighlights: {} + indexableObject: collection1, + hitHighlights: {} }, { - dspaceObject: { - name: 'Test collection 2', - uuid: 'collection2' - }, hitHighlights: {} + indexableObject: collection2, + hitHighlights: {} } ]))); } @@ -98,14 +107,14 @@ describe('ItemMoveComponent', () => { displayValue: 'Test collection 1', value: { name: 'Test collection 1', - id: 'collection1', + object: collection1, } }, { displayValue: 'Test collection 2', value: { name: 'Test collection 2', - id: 'collection2', + object: collection2, } } ]; @@ -121,24 +130,23 @@ describe('ItemMoveComponent', () => { it('should on click select the correct collection name and id', () => { const data = { name: 'Test collection 1', - id: 'collection1', + object: collection1, }; comp.onClick(data); expect(comp.selectedCollection).toEqual('Test collection 1'); - expect(comp.selectedCollectionId).toEqual('collection1'); + expect(comp.selectedCollectionObject).toEqual(collection1); }); describe('moveCollection', () => { it('should call itemDataService.moveToCollection', () => { comp.itemId = 'item-id'; - comp.selectedCollectionId = 'selected-collection-id'; + comp.selectedCollection = 'selected-collection-id'; + comp.selectedCollectionObject = collection1; comp.moveCollection(); - expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', 'selected-collection-id'); + expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1); }); it('should call notificationsService success message on success', () => { - // spyOn(notificationsServiceStub, 'success'); - comp.moveCollection(); expect(notificationsServiceStub.success).toHaveBeenCalled(); @@ -170,8 +178,6 @@ describe('ItemMoveComponent', () => { }); it('should call notificationsService error message on fail', () => { - // spyOn(notificationsServiceStub, 'error'); - comp.moveCollection(); expect(notificationsServiceStub.error).toHaveBeenCalled(); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 4f6f9bbe08..4cca0cd3a4 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -37,7 +37,6 @@ export class ItemMoveComponent implements OnInit { selectedCollection: string; selectedCollectionObject: Collection; - selectedCollectionId: string; itemId: string; constructor(private route: ActivatedRoute, @@ -78,8 +77,8 @@ export class ItemMoveComponent implements OnInit { map((rd: RemoteData>>) => { return rd.payload.page.map((searchResult) => { return { - displayValue: searchResult.dspaceObject.name, - value: {name: searchResult.dspaceObject.name, id: searchResult.dspaceObject.uuid} + displayValue: searchResult.indexableObject.name, + value: {name: searchResult.indexableObject.name, object: searchResult.indexableObject} }; }); }) @@ -93,8 +92,7 @@ export class ItemMoveComponent implements OnInit { */ onClick(data: any): void { this.selectedCollection = data.name; - this.selectedCollectionId = data.id; - this.selectedCollectionObject = data; + this.selectedCollectionObject = data.object; } /** From a419e64cef6f1e72448ef8d6130acd7466865f42 Mon Sep 17 00:00:00 2001 From: Philip Vissenaekens Date: Tue, 28 May 2019 17:13:18 +0200 Subject: [PATCH 032/149] 62571: removed duplicate line --- .../item-operation/item-operation.component.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 9b0c516083..1901bf5fb4 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -20,8 +20,6 @@ describe('ItemOperationComponent', () => { beforeEach(() => { itemOperation = new ItemOperation('key1', 'url1'); - itemOperation = new ItemOperation('key1', 'url1'); - fixture = TestBed.createComponent(ItemOperationComponent); comp = fixture.componentInstance; comp.operation = itemOperation; From 16bad8256475dd351bd8ee9146a2a3ef2d487813 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 6 Jun 2019 18:00:54 +0200 Subject: [PATCH 033/149] 62741: Entities grid templates - publication, orgunit, person, project --- .../orgunit-grid-element.component.html | 33 +++++++++++++++++ .../orgunit-grid-element.component.scss | 0 .../orgunit-grid-element.component.spec.ts | 0 .../orgunit/orgunit-grid-element.component.ts | 14 +++++++ .../person/person-grid-element.component.html | 30 +++++++++++++++ .../person/person-grid-element.component.scss | 0 .../person-grid-element.component.spec.ts | 0 .../person/person-grid-element.component.ts | 14 +++++++ .../project-grid-element.component.html | 25 +++++++++++++ .../project-grid-element.component.scss | 0 .../project-grid-element.component.spec.ts | 0 .../project/project-grid-element.component.ts | 14 +++++++ .../research-entities.module.ts | 8 +++- src/app/shared/items/item-type-decorator.ts | 1 + .../grid-thumbnail.component.html | 3 +- .../grid-thumbnail.component.ts | 18 +++++++-- .../publication-grid-element.component.html | 34 +++++++++++++++++ .../publication-grid-element.component.scss | 0 ...publication-grid-element.component.spec.ts | 0 .../publication-grid-element.component.ts | 15 ++++++++ ...arch-result-grid-element.component.spec.ts | 0 ...em-search-result-grid-element.component.ts | 37 +++++++++++++++++++ ...-search-result-grid-element.component.html | 34 +---------------- ...em-search-result-grid-element.component.ts | 5 ++- .../search-result-grid-element.component.ts | 7 +++- ...em-search-result-list-element.component.ts | 2 +- .../item-type-badge.component.html | 3 ++ .../item-type-badge.component.ts | 10 +++++ ...-search-result-list-element.component.html | 4 +- src/app/shared/shared.module.ts | 8 +++- 30 files changed, 271 insertions(+), 48 deletions(-) create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.scss create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts create mode 100644 src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts create mode 100644 src/app/shared/object-list/item-type-badge/item-type-badge.component.html create mode 100644 src/app/shared/object-list/item-type-badge/item-type-badge.component.ts diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html new file mode 100644 index 0000000000..afb7f8da09 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -0,0 +1,33 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + {{dso.firstMetadataValue('orgunit.identifier.country')}} + , + {{dso.firstMetadataValue('orgunit.identifier.city')}} + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts new file mode 100644 index 0000000000..f0c87eb975 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('OrgUnit', ItemViewMode.Card) +@Component({ + selector: 'ds-orgunit-grid-element', + styleUrls: ['./orgunit-grid-element.component.scss'], + templateUrl: './orgunit-grid-element.component.html', + animations: [focusShadow] +}) +export class OrgunitGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html new file mode 100644 index 0000000000..05f1eefda1 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+ +

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts new file mode 100644 index 0000000000..3ec17c9ce5 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; +import { focusShadow } from '../../../../shared/animations/focus'; + +@rendersItemType('Person', ItemViewMode.Card) +@Component({ + selector: 'ds-person-grid-element', + styleUrls: ['./person-grid-element.component.scss'], + templateUrl: './person-grid-element.component.html', + animations: [focusShadow] +}) +export class PersonGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html new file mode 100644 index 0000000000..f01f0334d3 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -0,0 +1,25 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts new file mode 100644 index 0000000000..be246f0fbe --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Project', ItemViewMode.Card) +@Component({ + selector: 'ds-project-grid-element', + styleUrls: ['./project-grid-element.component.scss'], + templateUrl: './project-grid-element.component.html', + animations: [focusShadow] +}) +export class ProjectGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index ba28f174df..099fa2a6a3 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -11,6 +11,9 @@ import { PersonMetadataListElementComponent } from './item-list-elements/person/ import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component'; import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { PersonGridElementComponent } from './item-grid-elements/person/person-grid-element.component'; +import { OrgunitGridElementComponent } from './item-grid-elements/orgunit/orgunit-grid-element.component'; +import { ProjectGridElementComponent } from './item-grid-elements/project/project-grid-element.component'; const ENTRY_COMPONENTS = [ OrgunitComponent, @@ -20,7 +23,10 @@ const ENTRY_COMPONENTS = [ OrgUnitMetadataListElementComponent, PersonListElementComponent, PersonMetadataListElementComponent, - ProjectListElementComponent + ProjectListElementComponent, + PersonGridElementComponent, + OrgunitGridElementComponent, + ProjectGridElementComponent ]; @NgModule({ diff --git a/src/app/shared/items/item-type-decorator.ts b/src/app/shared/items/item-type-decorator.ts index 2420e71908..3a040ae5bf 100644 --- a/src/app/shared/items/item-type-decorator.ts +++ b/src/app/shared/items/item-type-decorator.ts @@ -3,6 +3,7 @@ import { MetadataRepresentationType } from '../../core/shared/metadata-represent export enum ItemViewMode { Element = 'element', + Card = 'card', Full = 'full', Metadata = 'metadata' } diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html index c0c3c1f65f..5b09d09a55 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html @@ -1,4 +1,3 @@
- - +
diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts index 8ca93470da..6ae0c2d37e 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../../core/shared/bitstream.model'; +import { hasValue } from '../../empty.util'; /** * This component renders a given Bitstream as a thumbnail. @@ -12,7 +13,7 @@ import { Bitstream } from '../../../core/shared/bitstream.model'; styleUrls: ['./grid-thumbnail.component.scss'], templateUrl: './grid-thumbnail.component.html' }) -export class GridThumbnailComponent { +export class GridThumbnailComponent implements OnInit { @Input() thumbnail: Bitstream; @@ -21,10 +22,19 @@ export class GridThumbnailComponent { /** * The default 'holder.js' image */ - holderSource = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; + @Input() defaultImage? = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; + src: string; errorHandler(event) { - event.currentTarget.src = this.holderSource; + event.currentTarget.src = this.defaultImage; + } + + ngOnInit(): void { + if (hasValue(this.thumbnail) && this.thumbnail.content) { + this.src = this.thumbnail.content; + } else { + this.src = this.defaultImage + } } } diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html new file mode 100644 index 0000000000..b0509eb5df --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -0,0 +1,34 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + {{dso.firstMetadataValue('dc.date.issued')}} + , + + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.scss b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts new file mode 100644 index 0000000000..18dcccd1d2 --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts @@ -0,0 +1,15 @@ +import { TypedItemSearchResultGridElementComponent } from '../typed-item-search-result-grid-element.component'; +import { DEFAULT_ITEM_TYPE, ItemViewMode, rendersItemType } from '../../../../items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../animations/focus'; + +@rendersItemType('Publication', ItemViewMode.Card) +@rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Card) +@Component({ + selector: 'ds-publication-grid-element', + styleUrls: ['./publication-grid-element.component.scss'], + templateUrl: './publication-grid-element.component.html', + animations: [focusShadow] +}) +export class PublicationGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts new file mode 100644 index 0000000000..f4f470c052 --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts @@ -0,0 +1,37 @@ +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { SearchResultGridElementComponent } from '../../search-result-grid-element/search-result-grid-element.component'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { Component, Inject } from '@angular/core'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; +import { hasValue } from '../../../empty.util'; +import { MetadataMap } from '../../../../core/shared/metadata.models'; + +/** + * A generic component for displaying item grid elements + */ +@Component({ + selector: 'ds-item-search-result-grid-element', + template: '' +}) +export class TypedItemSearchResultGridElementComponent extends SearchResultGridElementComponent { + item: Item; + + constructor( + protected truncatableService: TruncatableService, + @Inject(ITEM) public obj: Item | ItemSearchResult, + ) { + super(undefined, truncatableService); + if (hasValue((obj as any).indexableObject)) { + this.object = obj as ItemSearchResult; + this.dso = this.object.indexableObject; + } else { + this.object = { + indexableObject: obj as Item, + hitHighlights: new MetadataMap() + }; + this.dso = obj as Item; + } + this.item = this.dso; + } +} diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index c7e2f524f3..d433c7acf2 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -1,33 +1 @@ - -
- -
- - -
-
-
- -

-
-

- - {{dso.firstMetadataValue('dc.date.issued')}} - , - - - -

-

- - - -

-
- View -
-
-
-
+ diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts index 30c36b3af9..7bbe41fe60 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts @@ -6,6 +6,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { SetViewMode } from '../../../view-mode'; import { focusShadow } from '../../../../shared/animations/focus'; +import { ItemViewMode } from '../../../items/item-type-decorator'; @Component({ selector: 'ds-item-search-result-grid-element', @@ -15,4 +16,6 @@ import { focusShadow } from '../../../../shared/animations/focus'; }) @renderElementsFor(ItemSearchResult, SetViewMode.Grid) -export class ItemSearchResultGridElementComponent extends SearchResultGridElementComponent {} +export class ItemSearchResultGridElementComponent extends SearchResultGridElementComponent { + viewMode = ItemViewMode.Card; +} diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 0961dc96ee..5f31d52ae7 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -7,6 +7,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m import { TruncatableService } from '../../truncatable/truncatable.service'; import { Observable } from 'rxjs'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { hasValue } from '../../empty.util'; @Component({ selector: 'ds-search-result-grid-element', @@ -16,9 +17,11 @@ import { Metadata } from '../../../core/shared/metadata.utils'; export class SearchResultGridElementComponent, K extends DSpaceObject> extends AbstractListableElementComponent { dso: K; - public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, private truncatableService: TruncatableService) { + public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, protected truncatableService: TruncatableService) { super(listableObject); - this.dso = this.object.indexableObject; + if (hasValue(this.object)) { + this.dso = this.object.indexableObject; + } } /** diff --git a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts index 7df3ab5681..dd1b5a7e5f 100644 --- a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts @@ -11,7 +11,7 @@ import { MetadataMap } from '../../../../core/shared/metadata.models'; * A generic component for displaying item list elements */ @Component({ - selector: 'ds-item-search-result', + selector: 'ds-item-search-result-list-element', template: '' }) export class TypedItemSearchResultListElementComponent extends SearchResultListElementComponent { diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.html b/src/app/shared/object-list/item-type-badge/item-type-badge.component.html new file mode 100644 index 0000000000..35d7663801 --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.html @@ -0,0 +1,3 @@ +
+ {{ type.toLowerCase() + '.listelement.badge' | translate }} +
diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts new file mode 100644 index 0000000000..53e36a535d --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts @@ -0,0 +1,10 @@ +import { Component, Input } from '@angular/core'; +import { ListableObject } from '../../object-collection/shared/listable-object.model'; + +@Component({ + selector: 'ds-item-type-badge', + templateUrl: './item-type-badge.component.html' +}) +export class ItemTypeBadgeComponent { + @Input() object: ListableObject; +} diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index a2617a956f..051a27bde7 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -1,4 +1,2 @@ -
- {{ type.toLowerCase() + '.listelement.badge' | translate }} -
+ diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 816139c8b9..66afb1e41b 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -138,6 +138,9 @@ import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; +import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; +import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; +import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -256,8 +259,10 @@ const COMPONENTS = [ CollectionSearchResultListElementComponent, ItemSearchResultListElementComponent, TypedItemSearchResultListElementComponent, + TypedItemSearchResultGridElementComponent, ItemTypeSwitcherComponent, - BrowseByComponent + BrowseByComponent, + ItemTypeBadgeComponent ]; const ENTRY_COMPONENTS = [ @@ -275,6 +280,7 @@ const ENTRY_COMPONENTS = [ CommunityGridElementComponent, SearchResultGridElementComponent, PublicationListElementComponent, + PublicationGridElementComponent, BrowseEntryListElementComponent, MyDSpaceResultDetailElementComponent, SearchResultGridElementComponent, From 22ae5a04e7959be8bd32f4aada2ba6a5a3bff23a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Jun 2019 12:51:27 +0200 Subject: [PATCH 034/149] 62741: Journal-types grid elements & grid thumbnail fix/improvement --- .../journal-issue-grid-element.component.html | 30 +++++++++++++++++ .../journal-issue-grid-element.component.scss | 0 ...urnal-issue-grid-element.component.spec.ts | 0 .../journal-issue-grid-element.component.ts | 14 ++++++++ ...journal-volume-grid-element.component.html | 30 +++++++++++++++++ ...journal-volume-grid-element.component.scss | 0 ...rnal-volume-grid-element.component.spec.ts | 0 .../journal-volume-grid-element.component.ts | 14 ++++++++ .../journal-grid-element.component.html | 33 +++++++++++++++++++ .../journal-grid-element.component.scss | 0 .../journal-grid-element.component.spec.ts | 0 .../journal/journal-grid-element.component.ts | 14 ++++++++ .../journal-entities.module.ts | 8 ++++- .../orgunit-grid-element.component.html | 2 +- .../person/person-grid-element.component.html | 2 +- .../project-grid-element.component.html | 2 +- .../publication-grid-element.component.html | 2 +- .../object-grid/object-grid.component.scss | 5 +++ 18 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts create mode 100644 src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html new file mode 100644 index 0000000000..6bd296db48 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts new file mode 100644 index 0000000000..538971bf84 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalIssue', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-issue-grid-element', + styleUrls: ['./journal-issue-grid-element.component.scss'], + templateUrl: './journal-issue-grid-element.component.html', + animations: [focusShadow] +}) +export class JournalIssueGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html new file mode 100644 index 0000000000..e1d3cb5e0a --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -0,0 +1,30 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts new file mode 100644 index 0000000000..3ef4e9948e --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalVolume', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-volume-grid-element', + styleUrls: ['./journal-volume-grid-element.component.scss'], + templateUrl: './journal-volume-grid-element.component.html', + animations: [focusShadow] +}) +export class JournalVolumeGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html new file mode 100644 index 0000000000..54fc0e4215 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -0,0 +1,33 @@ + +
+ +
+ + +
+
+
+ + +

+
+

+ + {{dso.firstMetadataValue('journal.contributor.editor')}} + , + {{dso.firstMetadataValue('journal.publisher')}} + +

+

+ + + +

+
+ View +
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts new file mode 100644 index 0000000000..37c0b0aad1 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts @@ -0,0 +1,14 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Journal', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-grid-element', + styleUrls: ['./journal-grid-element.component.scss'], + templateUrl: './journal-grid-element.component.html', + animations: [focusShadow] +}) +export class JournalGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index 50ec160650..4033645e1b 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -9,6 +9,9 @@ import { JournalListElementComponent } from './item-list-elements/journal/journa import { JournalIssueListElementComponent } from './item-list-elements/journal-issue/journal-issue-list-element.component'; import { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { JournalIssueGridElementComponent } from './item-grid-elements/journal-issue/journal-issue-grid-element.component'; +import { JournalVolumeGridElementComponent } from './item-grid-elements/journal-volume/journal-volume-grid-element.component'; +import { JournalGridElementComponent } from './item-grid-elements/journal/journal-grid-element.component'; const ENTRY_COMPONENTS = [ JournalComponent, @@ -16,7 +19,10 @@ const ENTRY_COMPONENTS = [ JournalVolumeComponent, JournalListElementComponent, JournalIssueListElementComponent, - JournalVolumeListElementComponent + JournalVolumeListElementComponent, + JournalIssueGridElementComponent, + JournalVolumeGridElementComponent, + JournalGridElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html index afb7f8da09..cb5d9b59af 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html index 05f1eefda1..463007d099 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html index f01f0334d3..1f122895ee 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html index b0509eb5df..06cf8496c4 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -2,7 +2,7 @@
- +
diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index ff78634863..fdd6c6e7ac 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -7,6 +7,11 @@ ds-wrapper-grid-element ::ng-deep { div.thumbnail > img { height: $card-thumbnail-height; width: 100%; + display: block; + min-width: 100%; + min-height: 100%; + object-fit: cover; + object-position: 50% 15%; } div.card { margin-top: $ds-wrapper-grid-spacing; From 0a32d3f91579daa2eaeb445a16efd4dd79cb3809 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Jun 2019 13:23:03 +0200 Subject: [PATCH 035/149] 62741: Remove whitespace and entity thumbnails from grid templates --- .../journal-issue-grid-element.component.html | 6 +++--- .../journal-volume-grid-element.component.html | 6 +++--- .../journal/journal-grid-element.component.html | 6 +++--- .../orgunit/orgunit-grid-element.component.html | 8 ++++---- .../person/person-grid-element.component.html | 8 ++++---- .../project/project-grid-element.component.html | 6 +++--- .../publication/publication-grid-element.component.html | 6 +++--- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html index 6bd296db48..11c1911eda 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -8,16 +8,16 @@
- +

- +

- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html index e1d3cb5e0a..aaa22d48f6 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -8,16 +8,16 @@
- +

- +

- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html index 54fc0e4215..d532fb60d4 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -8,19 +8,19 @@
- +

- + {{dso.firstMetadataValue('journal.contributor.editor')}} , {{dso.firstMetadataValue('journal.publisher')}}

- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html index cb5d9b59af..d4150bdc9a 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -2,23 +2,23 @@
- +
- +

- +

- + {{dso.firstMetadataValue('orgunit.identifier.country')}} , {{dso.firstMetadataValue('orgunit.identifier.city')}} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html index 463007d099..c47e96bcc1 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -2,22 +2,22 @@

- +
- +

- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html index 1f122895ee..16083c5787 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -2,17 +2,17 @@
- +
- +

- +

diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html index 06cf8496c4..3bb21b1f1c 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -8,12 +8,12 @@
- +

- + {{dso.firstMetadataValue('dc.date.issued')}} , @@ -21,7 +21,7 @@

- +

From 47994f955764beb08db5d0957eb52016c78c6afb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Jun 2019 15:18:38 +0200 Subject: [PATCH 036/149] 62741: Entities grid templates tests --- .../journal-issue-grid-element.component.html | 4 +- ...urnal-issue-grid-element.component.spec.ts | 47 +++++++ ...journal-volume-grid-element.component.html | 4 +- ...rnal-volume-grid-element.component.spec.ts | 47 +++++++ .../journal-grid-element.component.html | 6 +- .../journal-grid-element.component.spec.ts | 53 ++++++++ .../orgunit-grid-element.component.html | 6 +- .../orgunit-grid-element.component.spec.ts | 53 ++++++++ .../person/person-grid-element.component.html | 6 +- .../person-grid-element.component.spec.ts | 47 +++++++ .../project-grid-element.component.html | 2 +- .../project-grid-element.component.spec.ts | 41 ++++++ .../grid-thumbnail.component.spec.ts | 4 +- .../publication-grid-element.component.html | 2 +- ...publication-grid-element.component.spec.ts | 124 ++++++++++++++++++ ...arch-result-grid-element.component.spec.ts | 83 ++++++++++++ ...arch-result-grid-element.component.spec.ts | 89 ++----------- ...arch-result-list-element.component.spec.ts | 2 +- .../item-type-badge.component.spec.ts | 83 ++++++++++++ ...arch-result-list-element.component.spec.ts | 41 +----- 20 files changed, 612 insertions(+), 132 deletions(-) create mode 100644 src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html index 11c1911eda..93d3954f2f 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -11,12 +11,12 @@

-

+

-

+

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts index e69de29bb2..3af407d0f8 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalIssueGridElementComponent } from './journal-issue-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'journalissue.issuedate': [ + { + language: null, + value: '2015-06-26' + } + ], + 'journal.title': [ + { + language: 'en_US', + value: 'The journal title' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalIssueGridElementComponent', getEntityGridElementTestComponent(JournalIssueGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'journal-title'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html index aaa22d48f6..f5487a34cf 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -11,12 +11,12 @@

-

+

-

+

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts index e69de29bb2..4751ed4cd8 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalVolumeGridElementComponent } from './journal-volume-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'journalvolume.issuedate': [ + { + language: null, + value: '2015-06-26' + } + ], + 'journalvolume.identifier.description': [ + { + language: 'en_US', + value: 'A description for the journal volume' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalVolumeGridElementComponent', getEntityGridElementTestComponent(JournalVolumeGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'description'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html index d532fb60d4..d4354efcf4 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -14,12 +14,12 @@

- {{dso.firstMetadataValue('journal.contributor.editor')}} + {{dso.firstMetadataValue('journal.contributor.editor')}} , - {{dso.firstMetadataValue('journal.publisher')}} + {{dso.firstMetadataValue('journal.publisher')}}

-

+

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts index e69de29bb2..d9934e2d2f 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts @@ -0,0 +1,53 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalGridElementComponent } from './journal-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'journal.contributor.editor': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'journal.publisher': [ + { + language: 'en_US', + value: 'A company' + } + ], + 'journal.identifier.description': [ + { + language: 'en_US', + value: 'This is the description' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalGridElementComponent', getEntityGridElementTestComponent(JournalGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['editor', 'publisher', 'description'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html index d4150bdc9a..92fffd0166 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -11,7 +11,7 @@

-

+

@@ -19,9 +19,9 @@

- {{dso.firstMetadataValue('orgunit.identifier.country')}} + {{dso.firstMetadataValue('orgunit.identifier.country')}} , - {{dso.firstMetadataValue('orgunit.identifier.city')}} + {{dso.firstMetadataValue('orgunit.identifier.city')}}

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts index e69de29bb2..25249fd2b0 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts @@ -0,0 +1,53 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { OrgunitGridElementComponent } from './orgunit-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'orgunit.identifier.dateestablished': [ + { + language: null, + value: '2015-06-26' + } + ], + 'orgunit.identifier.country': [ + { + language: 'en_US', + value: 'Belgium' + } + ], + 'orgunit.identifier.city': [ + { + language: 'en_US', + value: 'Brussels' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('OrgunitGridElementComponent', getEntityGridElementTestComponent(OrgunitGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'country', 'city'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html index c47e96bcc1..badc241b65 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -11,12 +11,12 @@

- -

+

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts index e69de29bb2..b3343a0605 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { PersonGridElementComponent } from './person-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'person.identifier.email': [ + { + language: 'en_US', + value: 'Smith-Donald@gmail.com' + } + ], + 'person.identifier.jobtitle': [ + { + language: 'en_US', + value: 'Web Developer' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('PersonGridElementComponent', getEntityGridElementTestComponent(PersonGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['email', 'jobtitle'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html index 16083c5787..1fe4d18dae 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -11,7 +11,7 @@

-

+

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts index e69de29bb2..bcf19ed96a 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts @@ -0,0 +1,41 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { ProjectGridElementComponent } from './project-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'project.identifier.funder': [ + { + language: 'en_US', + value: 'The project funder' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('ProjectGridElementComponent', getEntityGridElementTestComponent(ProjectGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['funder'])); diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts index 2d2bd6305a..170ca34b42 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts @@ -6,7 +6,7 @@ import { GridThumbnailComponent } from './grid-thumbnail.component'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { SafeUrlPipe } from '../../utils/safe-url-pipe'; -describe('ThumbnailComponent', () => { +describe('GridThumbnailComponent', () => { let comp: GridThumbnailComponent; let fixture: ComponentFixture; let de: DebugElement; @@ -36,7 +36,7 @@ describe('ThumbnailComponent', () => { it('should display placeholder', () => { fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.holderSource); + expect(image.getAttribute('src')).toBe(comp.defaultImage); }); }); diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html index 3bb21b1f1c..e2477524ca 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -20,7 +20,7 @@

-

+

diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts index e69de29bb2..f067a21ae0 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts @@ -0,0 +1,124 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TruncatePipe } from '../../../../utils/truncate.pipe'; +import { TruncatableService } from '../../../../truncatable/truncatable.service'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { PublicationGridElementComponent } from './publication-grid-element.component'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { ITEM } from '../../../../items/switcher/item-type-switcher.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is an abstract' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('PublicationGridElementComponent', getEntityGridElementTestComponent(PublicationGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['authors', 'date', 'abstract'])); + +/** + * Create test cases for a grid component of an entity. + * @param component The component's class + * @param searchResultWithMetadata An ItemSearchResult containing an item with metadata that should be displayed in the grid element + * @param searchResultWithoutMetadata An ItemSearchResult containing an item that's missing the metadata that should be displayed in the grid element + * @param fieldsToCheck A list of fields to check. The tests expect to find html elements with class ".item-${field}", so make sure they exist in the html template of the grid element. + * For example: If one of the fields to check is labeled "authors", the html template should contain at least one element with class ".item-authors" that's + * present when the author metadata is available. + */ +export function getEntityGridElementTestComponent(component, searchResultWithMetadata: ItemSearchResult, searchResultWithoutMetadata: ItemSearchResult, fieldsToCheck: string[]) { + return () => { + let comp; + let fixture; + + const truncatableServiceStub: any = { + isCollapsed: (id: number) => observableOf(true), + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + declarations: [component, TruncatePipe], + providers: [ + { provide: TruncatableService, useValue: truncatableServiceStub }, + {provide: ITEM, useValue: searchResultWithoutMetadata} + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(component, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(component); + comp = fixture.componentInstance; + })); + + fieldsToCheck.forEach((field) => { + describe(`when the item has "${field}" metadata`, () => { + beforeEach(() => { + comp.dso = searchResultWithMetadata.indexableObject; + fixture.detectChanges(); + }); + + it(`should show the "${field}" field`, () => { + const itemAuthorField = fixture.debugElement.query(By.css(`.item-${field}`)); + expect(itemAuthorField).not.toBeNull(); + }); + }); + + describe(`when the item has no "${field}" metadata`, () => { + beforeEach(() => { + comp.dso = searchResultWithoutMetadata.indexableObject; + fixture.detectChanges(); + }); + + it(`should not show the "${field}" field`, () => { + const itemAuthorField = fixture.debugElement.query(By.css(`.item-${field}`)); + expect(itemAuthorField).toBeNull(); + }); + }); + }); + } +} diff --git a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts index e69de29bb2..e4ace8d0b2 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts @@ -0,0 +1,83 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } 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 { PageInfo } from '../../../../core/shared/page-info.model'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { createRelationshipsObservable } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { of as observableOf } from 'rxjs'; +import { MetadataMap } from '../../../../core/shared/metadata.models'; +import { TypedItemSearchResultGridElementComponent } from './typed-item-search-result-grid-element.component'; + +const mockItem: Item = Object.assign(new Item(), { + bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + metadata: [], + relationships: createRelationshipsObservable() +}); +const mockSearchResult = { + indexableObject: mockItem as Item, + hitHighlights: new MetadataMap() +} as ItemSearchResult; + +describe('TypedItemSearchResultGridElementComponent', () => { + let comp: TypedItemSearchResultGridElementComponent; + let fixture: ComponentFixture; + + describe('when injecting an Item', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TypedItemSearchResultGridElementComponent, TruncatePipe], + providers: [ + {provide: TruncatableService, useValue: {}}, + {provide: ITEM, useValue: mockItem} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(TypedItemSearchResultGridElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(TypedItemSearchResultGridElementComponent); + comp = fixture.componentInstance; + })); + + it('should initiate item, object and dso correctly', () => { + expect(comp.item).toBe(mockItem); + expect(comp.dso).toBe(mockItem); + expect(comp.object.indexableObject).toBe(mockItem); + }) + }); + + describe('when injecting an ItemSearchResult', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TypedItemSearchResultGridElementComponent, TruncatePipe], + providers: [ + {provide: TruncatableService, useValue: {}}, + {provide: ITEM, useValue: mockSearchResult} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(TypedItemSearchResultGridElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(TypedItemSearchResultGridElementComponent); + comp = fixture.componentInstance; + })); + + it('should initiate item, object and dso correctly', () => { + expect(comp.item).toBe(mockItem); + expect(comp.dso).toBe(mockItem); + expect(comp.object.indexableObject).toBe(mockItem); + }) + }); +}); diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts index 655fd268a7..282478ec33 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts @@ -8,6 +8,7 @@ import { Item } from '../../../../core/shared/item.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { ItemViewMode } from '../../../items/item-type-decorator'; let itemSearchResultGridElementComponent: ItemSearchResultGridElementComponent; let fixture: ComponentFixture; @@ -16,41 +17,17 @@ const truncatableServiceStub: any = { isCollapsed: (id: number) => observableOf(true), }; -const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult(); -mockItemWithAuthorAndDate.hitHighlights = {}; -mockItemWithAuthorAndDate.indexableObject = Object.assign(new Item(), { - bitstreams: observableOf({}), - metadata: { - 'dc.contributor.author': [ - { - language: 'en_US', - value: 'Smith, Donald' - } - ], - 'dc.date.issued': [ - { - language: null, - value: '2015-06-26' - } - ] - } -}); +const type = 'authorOfPublication'; -const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult(); -mockItemWithoutAuthorAndDate.hitHighlights = {}; -mockItemWithoutAuthorAndDate.indexableObject = Object.assign(new Item(), { +const mockItemWithRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithRelationshipType.hitHighlights = {}; +mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { bitstreams: observableOf({}), metadata: { - 'dc.title': [ + 'relationship.type': [ { language: 'en_US', - value: 'This is just another title' - } - ], - 'dc.type': [ - { - language: null, - value: 'Article' + value: type } ] } @@ -63,7 +40,7 @@ describe('ItemSearchResultGridElementComponent', () => { declarations: [ItemSearchResultGridElementComponent, TruncatePipe], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) } + { provide: 'objectElementProvider', useValue: (mockItemWithRelationshipType) } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemSearchResultGridElementComponent, { @@ -76,51 +53,9 @@ describe('ItemSearchResultGridElementComponent', () => { itemSearchResultGridElementComponent = fixture.componentInstance; })); - describe('When the item has an author', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should show the author paragraph', () => { - const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); - expect(itemAuthorField).not.toBeNull(); - }); - }); - - describe('When the item has no author', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should not show the author paragraph', () => { - const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); - expect(itemAuthorField).toBeNull(); - }); - }); - - describe('When the item has an issuedate', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should show the issuedate span', () => { - const itemAuthorField = fixture.debugElement.query(By.css('span.item-date')); - expect(itemAuthorField).not.toBeNull(); - }); - }); - - describe('When the item has no issuedate', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should not show the issuedate span', () => { - const dateField = fixture.debugElement.query(By.css('span.item-date')); - expect(dateField).toBeNull(); - }); + it('should show send the object to item-type-switcher using viewMode "Card"', () => { + const itemTypeSwitcherComp = fixture.debugElement.query(By.css('ds-item-type-switcher')).componentInstance; + expect(itemTypeSwitcherComp.object).toBe(mockItemWithRelationshipType); + expect(itemTypeSwitcherComp.viewMode).toEqual(ItemViewMode.Card); }); }); diff --git a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts index f320ff2efc..b100f584e2 100644 --- a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts @@ -23,7 +23,7 @@ const mockSearchResult = { hitHighlights: new MetadataMap() } as ItemSearchResult; -describe('ItemSearchResultComponent', () => { +describe('TypedItemSearchResultListElementComponent', () => { let comp: TypedItemSearchResultListElementComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts b/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts new file mode 100644 index 0000000000..04c40b73ff --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts @@ -0,0 +1,83 @@ +import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ItemTypeBadgeComponent } from './item-type-badge.component'; +import { By } from '@angular/platform-browser'; + +let comp: ItemTypeBadgeComponent; +let fixture: ComponentFixture; + +const type = 'authorOfPublication'; + +const mockItemWithRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithRelationshipType.hitHighlights = {}; +mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'relationship.type': [ + { + language: 'en_US', + value: type + } + ] + } +}); + +const mockItemWithoutRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutRelationshipType.hitHighlights = {}; +mockItemWithoutRelationshipType.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('ItemTypeBadgeComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemTypeBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemTypeBadgeComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(ItemTypeBadgeComponent); + comp = fixture.componentInstance; + })); + + describe('When the item has a relationship type', () => { + beforeEach(() => { + comp.object = mockItemWithRelationshipType; + fixture.detectChanges(); + }); + + it('should show the relationship type badge', () => { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge.nativeElement.textContent).toContain(type.toLowerCase()); + }); + }); + + describe('When the item has no relationship type', () => { + beforeEach(() => { + comp.object = mockItemWithoutRelationshipType; + fixture.detectChanges(); + }); + + it('should not show a badge', () => { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts index a370d3a632..8f41018404 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts @@ -33,20 +33,6 @@ mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { } }); -const mockItemWithoutRelationshipType: ItemSearchResult = new ItemSearchResult(); -mockItemWithoutRelationshipType.hitHighlights = {}; -mockItemWithoutRelationshipType.indexableObject = Object.assign(new Item(), { - bitstreams: observableOf({}), - metadata: { - 'dc.title': [ - { - language: 'en_US', - value: 'This is just another title' - } - ] - } -}); - describe('ItemSearchResultListElementComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ @@ -54,7 +40,7 @@ describe('ItemSearchResultListElementComponent', () => { declarations: [ItemSearchResultListElementComponent, TruncatePipe], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockItemWithoutRelationshipType) } + { provide: 'objectElementProvider', useValue: (mockItemWithRelationshipType) } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemSearchResultListElementComponent, { @@ -67,27 +53,8 @@ describe('ItemSearchResultListElementComponent', () => { itemSearchResultListElementComponent = fixture.componentInstance; })); - describe('When the item has a relationship type', () => { - beforeEach(() => { - itemSearchResultListElementComponent.object = mockItemWithRelationshipType; - fixture.detectChanges(); - }); - - it('should show the relationship type badge', () => { - const badge = fixture.debugElement.query(By.css('span.badge')); - expect(badge.nativeElement.textContent).toContain(type.toLowerCase()); - }); - }); - - describe('When the item has no relationship type', () => { - beforeEach(() => { - itemSearchResultListElementComponent.object = mockItemWithoutRelationshipType; - fixture.detectChanges(); - }); - - it('should not show a badge', () => { - const badge = fixture.debugElement.query(By.css('span.badge')); - expect(badge).toBeNull(); - }); + it('should show a badge on top of the list element', () => { + const badge = fixture.debugElement.query(By.css('ds-item-type-badge')).componentInstance; + expect(badge.object).toBe(mockItemWithRelationshipType); }); }); From 37fd04593b861cf82e45f9e762ec9a568d3ee06a Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 14 Jun 2019 16:15:45 +0200 Subject: [PATCH 037/149] 62355: replaced all method calls in templates (except for metadata values...) --- .../search-authority-filter.component.html | 2 +- .../search-facet-option.component.html | 2 +- .../search-facet-option.component.ts | 4 +- .../search-facet-range-option.component.html | 2 +- .../search-facet-range-option.component.ts | 5 +- ...earch-facet-selected-option.component.html | 2 +- .../search-facet-selected-option.component.ts | 5 +- .../search-facet-filter.component.ts | 13 ++-- .../search-filter/search-filter.service.ts | 6 +- .../search-hierarchy-filter.component.html | 2 +- .../search-range-filter.component.html | 2 +- .../search-text-filter.component.html | 2 +- .../search-filters.component.html | 2 +- .../search-filters.component.ts | 5 +- .../search-label/search-label.component.html | 6 ++ .../search-label/search-label.component.ts | 75 +++++++++++++++++++ .../search-labels.component.html | 12 +-- .../search-labels/search-labels.component.ts | 45 +---------- .../+search-page/search-page.component.html | 6 +- src/app/+search-page/search-page.component.ts | 9 ++- src/app/+search-page/search-page.module.ts | 2 + .../item-type-switcher.component.html | 2 +- .../switcher/item-type-switcher.component.ts | 6 +- .../object-collection.component.html | 6 +- .../object-collection.component.spec.ts | 8 +- .../object-collection.component.ts | 38 +++------- .../wrapper-detail-element.component.html | 2 +- .../wrapper-detail-element.component.ts | 6 +- ...-search-result-grid-element.component.html | 2 +- .../search-result-grid-element.component.ts | 4 +- .../wrapper-grid-element.component.html | 2 +- .../wrapper-grid-element.component.ts | 3 +- .../search-result-list-element.component.ts | 2 + .../wrapper-list-element.component.html | 2 +- .../wrapper-list-element.component.ts | 4 +- 35 files changed, 166 insertions(+), 130 deletions(-) create mode 100644 src/app/+search-page/search-labels/search-label/search-label.component.html create mode 100644 src/app/+search-page/search-labels/search-label/search-label.component.ts diff --git a/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index 76cdc6c8f5..63d034c6ea 100644 --- a/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -17,7 +17,7 @@

{{filterValue.value}} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts index 1fccee3736..1488f7a1e1 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts @@ -50,6 +50,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { */ addQueryParams; + searchLink: string; /** * Subscription to unsubscribe from on destroy */ @@ -66,6 +67,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { * Initializes all observable instance variables and starts listening to them */ ngOnInit(): void { + this.searchLink = this.getSearchLink(); this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked)); this.sub = observableCombineLatest(this.selectedValues$, this.searchConfigService.searchOptions) .subscribe(([selectedValues, searchOptions]) => { @@ -83,7 +85,7 @@ export class SearchFacetOptionComponent implements OnInit, OnDestroy { /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ - public getSearchLink(): string { + private getSearchLink(): string { if (this.inPlaceSearch) { return './'; } diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html index 8e8ad9b4e3..577e3e3c1c 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html @@ -1,5 +1,5 @@ {{filterValue.label}} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts index 54d5d535df..1c243adfee 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts @@ -55,6 +55,8 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { */ sub: Subscription; + searchLink: string; + constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected searchConfigService: SearchConfigurationService, @@ -66,6 +68,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { * Initializes all observable instance variables and starts listening to them */ ngOnInit(): void { + this.searchLink = this.getSearchLink(); this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked)); this.sub = this.searchConfigService.searchOptions.subscribe(() => { this.updateChangeParams() @@ -82,7 +85,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ - public getSearchLink(): string { + private getSearchLink(): string { if (this.inPlaceSearch) { return './'; } diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html index 5657bd224e..5198433207 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html @@ -1,5 +1,5 @@ {{selectedValue.label}} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts index 78dde92c2b..123a32dfb4 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.ts @@ -49,6 +49,8 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { */ sub: Subscription; + searchLink: string; + constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected searchConfigService: SearchConfigurationService, @@ -64,12 +66,13 @@ export class SearchFacetSelectedOptionComponent implements OnInit, OnDestroy { .subscribe(([selectedValues, searchOptions]) => { this.updateRemoveParams(selectedValues) }); + this.searchLink = this.getSearchLink(); } /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ - public getSearchLink(): string { + private getSearchLink(): string { if (this.inPlaceSearch) { return './'; } 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 ee980a0599..ccbda54f89 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 @@ -80,6 +80,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { */ searchOptions$: Observable; + /** + * The current URL + */ + currentUrl: string; + constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected rdbs: RemoteDataBuildService, @@ -93,6 +98,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { * Initializes all observable instance variables and starts listening to them */ ngOnInit(): void { + this.currentUrl = this.router.url; this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined)); this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged()); @@ -215,13 +221,6 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { return this.filterService.getPage(this.filterConfig.name); } - /** - * @returns {string} the current URL - */ - getCurrentUrl() { - return this.router.url; - } - /** * Submits a new active custom value to the filter from the input field * @param data The string from the input field diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 4b12417084..6024ad7249 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -1,5 +1,5 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { mergeMap, map, distinctUntilChanged } from 'rxjs/operators'; +import { distinctUntilChanged, map, mergeMap } from 'rxjs/operators'; import { Injectable, InjectionToken } from '@angular/core'; import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; @@ -17,12 +17,8 @@ import { SearchFilterConfig } from '../../search-service/search-filter-config.mo import { RouteService } from '../../../shared/services/route.service'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SearchOptions } from '../../search-options.model'; -import { PaginatedSearchOptions } from '../../paginated-search-options.model'; import { SearchFixedFilterService } from './search-fixed-filter.service'; import { Params } from '@angular/router'; -import * as postcss from 'postcss'; -import prefix = postcss.vendor.prefix; // const spy = create(); const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index ac2a72f4b6..996fd7f751 100644 --- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -17,7 +17,7 @@
+ [action]="currentUrl">
-
{{"search.filters.reset" | translate}} +{{"search.filters.reset" | translate}} diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index e970647747..ba63d143c6 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -37,6 +37,8 @@ export class SearchFiltersComponent implements OnInit { */ @Input() inPlaceSearch; + searchLink: string; + /** * Initialize instance variables * @param {SearchService} searchService @@ -60,12 +62,13 @@ export class SearchFiltersComponent implements OnInit { Object.keys(filters).forEach((f) => filters[f] = null); return filters; })); + this.searchLink = this.getSearchLink(); } /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ - public getSearchLink(): string { + private getSearchLink(): string { if (this.inPlaceSearch) { return './'; } diff --git a/src/app/+search-page/search-labels/search-label/search-label.component.html b/src/app/+search-page/search-labels/search-label/search-label.component.html new file mode 100644 index 0000000000..391efcb763 --- /dev/null +++ b/src/app/+search-page/search-labels/search-label/search-label.component.html @@ -0,0 +1,6 @@ + + {{('search.filters.applied.' + key) | translate}}: {{normalizeFilterValue(value)}} + × + \ No newline at end of file diff --git a/src/app/+search-page/search-labels/search-label/search-label.component.ts b/src/app/+search-page/search-labels/search-label/search-label.component.ts new file mode 100644 index 0000000000..ab58e1bf4e --- /dev/null +++ b/src/app/+search-page/search-labels/search-label/search-label.component.ts @@ -0,0 +1,75 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Params } from '@angular/router'; +import { SearchService } from '../../search-service/search.service'; +import { map } from 'rxjs/operators'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; + +@Component({ + selector: 'ds-search-label', + templateUrl: './search-label.component.html', +}) + +/** + * Component that represents the labels containing the currently active filters + */ +export class SearchLabelComponent implements OnInit { + @Input() key: string; + @Input() value: string; + @Input() inPlaceSearch: boolean; + @Input() appliedFilters: Observable; + searchLink: string; + removeParameters: Observable; + + /** + * Initialize the instance variable + */ + constructor( + private searchService: SearchService) { + } + + ngOnInit(): void { + this.searchLink = this.getSearchLink(); + this.removeParameters = this.getRemoveParams(); + } + + /** + * Calculates the parameters that should change if a given value for the given filter would be removed from the active filters + * @returns {Observable} The changed filter parameters + */ + getRemoveParams(): Observable { + return this.appliedFilters.pipe( + map((filters) => { + const field: string = Object.keys(filters).find((f) => f === this.key); + const newValues = hasValue(filters[field]) ? filters[field].filter((v) => v !== this.value) : null; + return { + [field]: isNotEmpty(newValues) ? newValues : null, + page: 1 + }; + }) + ) + } + + /** + * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true + */ + private getSearchLink(): string { + if (this.inPlaceSearch) { + return './'; + } + return this.searchService.getSearchLink(); + } + + /** + * TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved + * Strips authority operator from filter value + * e.g. 'test ,authority' => 'test' + * + * @param value + */ + normalizeFilterValue(value: string) { + // const pattern = /,[^,]*$/g; + const pattern = /,authority*$/g; + return value.replace(pattern, ''); + } +} diff --git a/src/app/+search-page/search-labels/search-labels.component.html b/src/app/+search-page/search-labels/search-labels.component.html index cac81e8717..6a668826da 100644 --- a/src/app/+search-page/search-labels/search-labels.component.html +++ b/src/app/+search-page/search-labels/search-labels.component.html @@ -1,13 +1,7 @@ diff --git a/src/app/+search-page/search-labels/search-labels.component.ts b/src/app/+search-page/search-labels/search-labels.component.ts index 104ed5b08b..5f95525bed 100644 --- a/src/app/+search-page/search-labels/search-labels.component.ts +++ b/src/app/+search-page/search-labels/search-labels.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; import { SearchService } from '../search-service/search.service'; import { Observable } from 'rxjs'; import { Params } from '@angular/router'; @@ -31,50 +31,7 @@ export class SearchLabelsComponent { * Initialize the instance variable */ constructor( - private searchService: SearchService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService) { this.appliedFilters = this.searchConfigService.getCurrentFrontendFilters(); } - - /** - * Calculates the parameters that should change if a given value for the given filter would be removed from the active filters - * @param {string} filterField The filter field parameter name from which the value should be removed - * @param {string} filterValue The value that is removed for this given filter field - * @returns {Observable} The changed filter parameters - */ - getRemoveParams(filterField: string, filterValue: string): Observable { - return this.appliedFilters.pipe( - map((filters) => { - const field: string = Object.keys(filters).find((f) => f === filterField); - const newValues = hasValue(filters[field]) ? filters[field].filter((v) => v !== filterValue) : null; - return { - [field]: isNotEmpty(newValues) ? newValues : null, - page: 1 - }; - }) - ) - } - - /** - * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true - */ - public getSearchLink(): string { - if (this.inPlaceSearch) { - return './'; - } - return this.searchService.getSearchLink(); - } - - /** - * TODO to review after https://github.com/DSpace/dspace-angular/issues/368 is resolved - * Strips authority operator from filter value - * e.g. 'test ,authority' => 'test' - * - * @param value - */ - normalizeFilterValue(value: string) { - // const pattern = /,[^,]*$/g; - const pattern = /,authority*$/g; - return value.replace(pattern, ''); - } } diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index b4d8c70f11..fc4c2dce09 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -7,7 +7,7 @@ @@ -15,12 +15,12 @@
+ [@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'"> + [ngClass]="{'active': !(isSidebarCollapsed$ | async)}">
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index f23bff96f3..5e0a5ab9a2 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -91,6 +91,9 @@ export class SearchPageComponent implements OnInit { @Input() fixedFilter$: Observable; + searchLink: string; + isSidebarCollapsed$: Observable; + constructor(protected service: SearchService, protected sidebarService: SearchSidebarService, protected windowService: HostWindowService, @@ -107,6 +110,8 @@ export class SearchPageComponent implements OnInit { * If something changes, update the list of scopes for the dropdown */ ngOnInit(): void { + this.isSidebarCollapsed$ = this.isSidebarCollapsed(); + this.searchLink = this.getSearchLink(); this.searchOptions$ = this.getSearchOptions(); this.sub = this.searchOptions$.pipe( switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(observableOf(undefined))))) @@ -147,14 +152,14 @@ export class SearchPageComponent implements OnInit { * Check if the sidebar is collapsed * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded */ - public isSidebarCollapsed(): Observable { + private isSidebarCollapsed(): Observable { return this.sidebarService.isCollapsed; } /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ - public getSearchLink(): string { + private getSearchLink(): string { if (this.inPlaceSearch) { return './'; } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 65558eae17..297bfc8b74 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -32,6 +32,7 @@ import { SearchFacetSelectedOptionComponent } from './search-filters/search-filt import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component'; import { SearchSwitchConfigurationComponent } from './search-switch-configuration/search-switch-configuration.component'; import { SearchAuthorityFilterComponent } from './search-filters/search-filter/search-authority-filter/search-authority-filter.component'; +import { SearchLabelComponent } from './search-labels/search-label/search-label.component'; const effects = [ SearchSidebarEffects @@ -49,6 +50,7 @@ const components = [ SearchFilterComponent, SearchFacetFilterComponent, SearchLabelsComponent, + SearchLabelComponent, SearchFacetFilterComponent, SearchFacetFilterWrapperComponent, SearchRangeFilterComponent, diff --git a/src/app/shared/items/switcher/item-type-switcher.component.html b/src/app/shared/items/switcher/item-type-switcher.component.html index 4965359495..f2ea5784fc 100644 --- a/src/app/shared/items/switcher/item-type-switcher.component.html +++ b/src/app/shared/items/switcher/item-type-switcher.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/items/switcher/item-type-switcher.component.ts b/src/app/shared/items/switcher/item-type-switcher.component.ts index 21a045b8f4..cd061bc1dd 100644 --- a/src/app/shared/items/switcher/item-type-switcher.component.ts +++ b/src/app/shared/items/switcher/item-type-switcher.component.ts @@ -32,6 +32,8 @@ export class ItemTypeSwitcherComponent implements OnInit { */ objectInjector: Injector; + component: any; + constructor(private injector: Injector) { } @@ -40,14 +42,14 @@ export class ItemTypeSwitcherComponent implements OnInit { providers: [{ provide: ITEM, useFactory: () => this.object, deps:[] }], parent: this.injector }); - + this.component = this.getComponent(); } /** * Fetch the component depending on the item's relationship type * @returns {string} */ - getComponent(): string { + private getComponent(): string { if (hasValue((this.object as any).representationType)) { const metadataRepresentation = this.object as MetadataRepresentation; return getComponentByItemType(metadataRepresentation.itemType, this.viewMode, metadataRepresentation.representationType); diff --git a/src/app/shared/object-collection/object-collection.component.html b/src/app/shared/object-collection/object-collection.component.html index 5ba889892a..a0878efa24 100644 --- a/src/app/shared/object-collection/object-collection.component.html +++ b/src/app/shared/object-collection/object-collection.component.html @@ -8,7 +8,7 @@ (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortFieldChange($event)" - *ngIf="getViewMode()===viewModeEnum.List"> + *ngIf="(currentMode$ | async) === viewModeEnum.List"> + *ngIf="(currentMode$ | async) === viewModeEnum.Grid"> + *ngIf="(currentMode$ | async) === viewModeEnum.Detail"> diff --git a/src/app/shared/object-collection/object-collection.component.spec.ts b/src/app/shared/object-collection/object-collection.component.spec.ts index aed2b2598d..3b30666757 100644 --- a/src/app/shared/object-collection/object-collection.component.spec.ts +++ b/src/app/shared/object-collection/object-collection.component.spec.ts @@ -1,10 +1,8 @@ import { ObjectCollectionComponent } from './object-collection.component'; import { SetViewMode } from '../view-mode'; -import { element } from 'protractor'; import { By } from '@angular/platform-browser'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Config } from '../../../config/config.interface'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { of as observableOf } from 'rxjs'; import { RouterStub } from '../testing/router-stub'; @@ -38,14 +36,14 @@ describe('ObjectCollectionComponent', () => { })); it('should only show the grid component when the viewmode is set to grid', () => { - objectCollectionComponent.currentMode = SetViewMode.Grid; + objectCollectionComponent.currentMode$ = observableOf(SetViewMode.Grid); expect(fixture.debugElement.query(By.css('ds-object-grid'))).toBeDefined(); expect(fixture.debugElement.query(By.css('ds-object-list'))).toBeNull(); }); it('should only show the list component when the viewmode is set to list', () => { - objectCollectionComponent.currentMode = SetViewMode.List; + objectCollectionComponent.currentMode$ = observableOf(SetViewMode.List); expect(fixture.debugElement.query(By.css('ds-object-list'))).toBeDefined(); expect(fixture.debugElement.query(By.css('ds-object-grid'))).toBeNull(); diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index ccc1de1f2f..526fc95781 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -11,7 +11,7 @@ import { import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, map, startWith } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { PageInfo } from '../../core/shared/page-info.model'; @@ -19,14 +19,14 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { ListableObject } from './shared/listable-object.model'; import { SetViewMode } from '../view-mode'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; @Component({ selector: 'ds-viewable-collection', styleUrls: ['./object-collection.component.scss'], templateUrl: './object-collection.component.html', }) -export class ObjectCollectionComponent implements OnChanges, OnInit { +export class ObjectCollectionComponent implements OnInit { @Input() objects: RemoteData; @Input() config?: PaginationComponentOptions; @@ -34,7 +34,6 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { @Input() hasBorder = false; @Input() hideGear = false; pageInfo: Observable; - private sub; /** * An event fired when the page is changed. * Event's payload equals to the newly selected page. @@ -61,25 +60,17 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { */ @Output() sortFieldChange: EventEmitter = new EventEmitter(); data: any = {}; - currentMode: SetViewMode = SetViewMode.List; + currentMode$: Observable; viewModeEnum = SetViewMode; - ngOnChanges(changes: SimpleChanges) { - if (changes.objects && !changes.objects.isFirstChange()) { - // this.pageInfo = this.objects.pageInfo; - } - } - ngOnInit(): void { - // this.pageInfo = this.objects.pageInfo; - - this.sub = this.route + this.currentMode$ = this.route .queryParams - .subscribe((params) => { - if (isNotEmpty(params.view)) { - this.currentMode = params.view; - } - }); + .pipe( + filter((params) => isNotEmpty(params.view)), + map((params) => params.view), + startWith(SetViewMode.List) + ); } /** @@ -96,15 +87,6 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { private router: Router) { } - getViewMode(): SetViewMode { - this.route.queryParams.pipe(map((params) => { - if (isNotEmpty(params.view) && hasValue(params.view)) { - this.currentMode = params.view; - } - })); - return this.currentMode; - } - onPageChange(event) { this.pageChange.emit(event); } diff --git a/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.html b/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.html index 00a8ed2dc8..ef7254b97c 100644 --- a/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.html +++ b/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.ts b/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.ts index 92b30f9ce7..2ca8069b16 100644 --- a/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.ts +++ b/src/app/shared/object-detail/wrapper-detail-element/wrapper-detail-element.component.ts @@ -26,6 +26,8 @@ export class WrapperDetailElementComponent implements OnInit { */ objectInjector: Injector; + detailElement: any; + /** * Initialize instance variables * @@ -42,13 +44,13 @@ export class WrapperDetailElementComponent implements OnInit { providers: [{ provide: 'objectElementProvider', useFactory: () => (this.object), deps:[] }], parent: this.injector }); - + this.detailElement = this.getDetailElement(); } /** * Return class name for the object to inject */ - getDetailElement(): string { + private getDetailElement(): string { const f: GenericConstructor = this.object.constructor as GenericConstructor; return rendersDSOType(f, SetViewMode.Detail); } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index c7e2f524f3..ecbf8f706e 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 0961dc96ee..2e19f2fdf9 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -15,10 +15,12 @@ import { Metadata } from '../../../core/shared/metadata.utils'; export class SearchResultGridElementComponent, K extends DSpaceObject> extends AbstractListableElementComponent { dso: K; + isCollapsed$: Observable; public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, private truncatableService: TruncatableService) { super(listableObject); this.dso = this.object.indexableObject; + this.isCollapsed$ = this.isCollapsed(); } /** @@ -41,7 +43,7 @@ export class SearchResultGridElementComponent, K exten return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } - isCollapsed(): Observable { + private isCollapsed(): Observable { return this.truncatableService.isCollapsed(this.dso.id); } diff --git a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.html b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.html index b613b16055..d6fd1cf9aa 100644 --- a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.html +++ b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts index 84f9357b2d..0a7312484f 100644 --- a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts +++ b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts @@ -12,6 +12,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m export class WrapperGridElementComponent implements OnInit { @Input() object: ListableObject; objectInjector: Injector; + gridElement: any; constructor(private injector: Injector) { } @@ -21,7 +22,7 @@ export class WrapperGridElementComponent implements OnInit { providers: [{ provide: 'objectElementProvider', useFactory: () => (this.object), deps:[] }], parent: this.injector }); - + this.gridElement = this.getGridElement(); } getGridElement(): string { diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 227d375f2a..7017f3f48b 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -8,6 +8,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { TruncatableService } from '../../truncatable/truncatable.service'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { MetadataMap } from '../../../core/shared/metadata.models'; @Component({ selector: 'ds-search-result-list-element', @@ -16,6 +17,7 @@ import { Metadata } from '../../../core/shared/metadata.utils'; export class SearchResultListElementComponent, K extends DSpaceObject> extends AbstractListableElementComponent { dso: K; + metadata: MetadataMap; public constructor(@Inject('objectElementProvider') public listable: ListableObject, protected truncatableService: TruncatableService) { super(listable); diff --git a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.html b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.html index d5cfebdfa5..db87596f31 100644 --- a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.html +++ b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts index 17e6f0fd85..29b1364a75 100644 --- a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts +++ b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts @@ -13,6 +13,7 @@ export class WrapperListElementComponent implements OnInit { @Input() object: ListableObject; @Input() index: number; objectInjector: Injector; + listElement: any; constructor(private injector: Injector) {} @@ -24,9 +25,10 @@ export class WrapperListElementComponent implements OnInit { ], parent: this.injector }); + this.listElement = this.getListElement(); } - getListElement(): string { + private getListElement(): string { const f: GenericConstructor = this.object.constructor as GenericConstructor; return rendersDSOType(f, SetViewMode.List); } From bb76015aa12886337fb65154630d7fd925b8d7f7 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 18 Jun 2019 08:58:35 +0200 Subject: [PATCH 038/149] Solved issue with non-existing search pages --- .../create-collection-page.component.spec.ts | 2 +- .../create-collection-page.component.ts | 2 +- .../create-community-page.component.spec.ts | 2 +- .../create-community-page.component.ts | 2 +- .../delete-community-page.component.spec.ts | 2 +- .../my-dspace-configuration.service.ts | 2 +- .../my-dspace-page.component.spec.ts | 2 +- .../filtered-search-page.component.ts | 7 +++- .../filtered-search-page.guard.ts | 37 +++++++++++++++++-- .../search-filter/search-filter.service.ts | 2 +- .../search-fixed-filter.service.spec.ts | 2 +- .../search-fixed-filter.service.ts | 4 +- .../search-range-filter.component.spec.ts | 2 +- .../search-range-filter.component.ts | 2 +- .../search-page-routing.module.ts | 9 ++++- .../search-page.component.spec.ts | 2 +- src/app/+search-page/search-page.component.ts | 10 ++--- .../search-configuration.service.ts | 2 +- .../search-service/search.service.spec.ts | 2 +- .../search-service/search.service.ts | 2 +- src/app/app.component.spec.ts | 4 +- src/app/app.component.ts | 4 +- src/app/core/auth/auth.service.spec.ts | 6 +-- src/app/core/auth/auth.service.ts | 4 +- src/app/core/core.effects.ts | 2 +- src/app/core/core.module.ts | 8 ++-- src/app/core/core.reducers.ts | 2 +- .../{shared => core}/services/api.service.ts | 0 .../services/client-cookie.service.ts | 0 .../services/cookie.service.spec.ts | 0 .../services/cookie.service.ts | 0 .../services/route.actions.ts | 0 .../services/route.effects.ts | 0 .../services/route.reducer.ts | 0 .../services/route.service.spec.ts | 4 +- .../services/route.service.ts | 10 ++--- .../services/server-cookie.service.ts | 0 .../services/server-response.service.ts | 0 .../services/window.service.ts | 0 .../pagenotfound/pagenotfound.component.ts | 2 +- .../create-comcol-page.component.spec.ts | 2 +- .../create-comcol-page.component.ts | 2 +- src/app/submission/submission.module.ts | 2 +- src/app/submission/submission.service.spec.ts | 2 +- src/app/submission/submission.service.ts | 2 +- src/modules/app/browser-app.module.ts | 4 +- src/modules/app/server-app.module.ts | 4 +- 47 files changed, 99 insertions(+), 62 deletions(-) rename src/app/{shared => core}/services/api.service.ts (100%) rename src/app/{shared => core}/services/client-cookie.service.ts (100%) rename src/app/{shared => core}/services/cookie.service.spec.ts (100%) rename src/app/{shared => core}/services/cookie.service.ts (100%) rename src/app/{shared => core}/services/route.actions.ts (100%) rename src/app/{shared => core}/services/route.effects.ts (100%) rename src/app/{shared => core}/services/route.reducer.ts (100%) rename src/app/{shared => core}/services/route.service.spec.ts (97%) rename src/app/{shared => core}/services/route.service.ts (95%) rename src/app/{shared => core}/services/server-cookie.service.ts (100%) rename src/app/{shared => core}/services/server-response.service.ts (100%) rename src/app/{shared => core}/services/window.service.ts (100%) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index 29350a83e0..e223b11c65 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { SharedModule } from '../../shared/shared.module'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 94229b4932..2cab36d285 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index dba15dbe88..dead5a5c3b 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { SharedModule } from '../../shared/shared.module'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 828d8338af..fd5f18442a 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts index f18c4fb1f1..c23df93976 100644 --- a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts @@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { SharedModule } from '../../shared/shared.module'; import { of as observableOf } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; diff --git a/src/app/+my-dspace-page/my-dspace-configuration.service.ts b/src/app/+my-dspace-page/my-dspace-configuration.service.ts index 705ec897f8..39c7574407 100644 --- a/src/app/+my-dspace-page/my-dspace-configuration.service.ts +++ b/src/app/+my-dspace-page/my-dspace-configuration.service.ts @@ -8,7 +8,7 @@ import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value- import { RoleService } from '../core/roles/role.service'; import { SearchConfigurationOption } from '../+search-page/search-switch-configuration/search-configuration-option.model'; import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service'; diff --git a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts index 9658814a6a..d31d724b9e 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts @@ -17,7 +17,7 @@ import { HostWindowService } from '../shared/host-window.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { RemoteData } from '../core/data/remote-data'; import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { routeServiceStub } from '../shared/testing/route-service-stub'; import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub'; import { SearchService } from '../+search-page/search-service/search.service'; diff --git a/src/app/+search-page/filtered-search-page.component.ts b/src/app/+search-page/filtered-search-page.component.ts index 66c619b823..85d521ee15 100644 --- a/src/app/+search-page/filtered-search-page.component.ts +++ b/src/app/+search-page/filtered-search-page.component.ts @@ -4,12 +4,14 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SearchPageComponent } from './search-page.component'; import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { pushInOut } from '../shared/animations/push'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { SearchConfigurationService } from './search-service/search-configuration.service'; import { Observable } from 'rxjs'; import { PaginatedSearchOptions } from './paginated-search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { map } from 'rxjs/operators'; +import { isEmpty, isNotEmpty } from '../shared/empty.util'; +import { ActivatedRoute } from '@angular/router'; /** * This component renders a simple item page. @@ -53,6 +55,9 @@ export class FilteredSearchPageComponent extends SearchPageComponent implements * If something changes, update the list of scopes for the dropdown */ ngOnInit(): void { + if (isEmpty(this.fixedFilter$)) { + this.fixedFilter$ = this.routeService.getRouteParameterValue('filter'); + } super.ngOnInit(); } diff --git a/src/app/+search-page/filtered-search-page.guard.ts b/src/app/+search-page/filtered-search-page.guard.ts index 6d41d4965d..e28dadec71 100644 --- a/src/app/+search-page/filtered-search-page.guard.ts +++ b/src/app/+search-page/filtered-search-page.guard.ts @@ -1,6 +1,16 @@ import { Injectable } from '@angular/core'; -import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + CanActivate, + NavigationEnd, + Router, + RouterStateSnapshot +} from '@angular/router'; import { Observable } from 'rxjs'; +import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service'; +import { map, take, tap, filter } from 'rxjs/operators'; +import { isEmpty, isNotEmpty } from '../shared/empty.util'; +import { Location } from '@angular/common'; @Injectable() /** @@ -9,14 +19,33 @@ import { Observable } from 'rxjs'; * - filter: The current filter stored in route.params */ export class FilteredSearchPageGuard implements CanActivate { + constructor(private service: SearchFixedFilterService, private router: Router, private location: Location) { + } + canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean { - const filter = route.params.filter; + route.params = Object.assign({}, route.params, { filter: route.params.filter.toLowerCase() }); + const filterName = route.params.filter; - const newTitle = filter + '.search.title'; + const newTitle = filterName + '.search.title'; route.data = { title: newTitle }; - return true; + + return this.service.getQueryByFilterName(filterName).pipe( + tap((query) => { + if (isEmpty(query)) { + this.router.navigateByUrl('/404', { skipLocationChange: true }); + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + take(1) + ) + .subscribe(() => this.location.replaceState(state.url)); + } + } + ), + map((query) => isNotEmpty(query)) + ); } } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 6024ad7249..8482838101 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -14,7 +14,7 @@ import { } from './search-filter.actions'; import { hasValue, isNotEmpty, } from '../../../shared/empty.util'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; -import { RouteService } from '../../../shared/services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SearchFixedFilterService } from './search-fixed-filter.service'; diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts index 3f6c2ef133..a201d37d48 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts @@ -1,5 +1,5 @@ import { SearchFixedFilterService } from './search-fixed-filter.service'; -import { RouteService } from '../../../shared/services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { RequestService } from '../../../core/data/request.service'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index 0f17b508c9..85f637ce32 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -9,7 +9,6 @@ import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { FilteredDiscoveryPageResponseParsingService } from '../../../core/data/filtered-discovery-page-response-parsing.service'; import { hasValue } from '../../../shared/empty.util'; import { configureRequest, getResponseFromEntry } from '../../../core/shared/operators'; -import { RouteService } from '../../../shared/services/route.service'; import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response.models'; /** @@ -19,8 +18,7 @@ import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response.mod export class SearchFixedFilterService { private queryByFilterPath = 'filtered-discovery-pages'; - constructor(private routeService: RouteService, - protected requestService: RequestService, + constructor(protected requestService: RequestService, private halService: HALEndpointService) { } diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 119f3f92a9..2b69fe7f55 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -16,7 +16,7 @@ import { RouterStub } from '../../../../shared/testing/router-stub'; import { Router } from '@angular/router'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { SearchRangeFilterComponent } from './search-range-filter.component'; -import { RouteService } from '../../../../shared/services/route.service'; +import { RouteService } from '../../../../core/services/route.service'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service-stub'; diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index 95d7441184..5ac59b65f8 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -14,7 +14,7 @@ import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-f import { SearchService } from '../../../search-service/search.service'; import { Router } from '@angular/router'; import * as moment from 'moment'; -import { RouteService } from '../../../../shared/services/route.service'; +import { RouteService } from '../../../../core/services/route.service'; import { hasValue } from '../../../../shared/empty.util'; import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component'; diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index c3cf4e1343..e3a91c6f69 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -9,8 +9,13 @@ import { FilteredSearchPageGuard } from './filtered-search-page.guard'; imports: [ RouterModule.forChild([ { path: '', component: SearchPageComponent, data: { title: 'search.title' } }, - { path: ':filter', component: FilteredSearchPageComponent, canActivate: [FilteredSearchPageGuard]} + { + path: ':filter', + component: FilteredSearchPageComponent, + canActivate: [FilteredSearchPageGuard], + } ]) ] }) -export class SearchPageRoutingModule { } +export class SearchPageRoutingModule { +} diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index 88c7c693d3..2bc3d4071d 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -21,7 +21,7 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte import { SearchConfigurationService } from './search-service/search-configuration.service'; import { RemoteData } from '../core/data/remote-data'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service-stub'; import { PaginatedSearchOptions } from './paginated-search-options.model'; import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service'; diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 5e0a5ab9a2..8fe38eaebb 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -13,7 +13,7 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { SearchConfigurationService } from './search-service/search-configuration.service'; import { getSucceededRemoteData } from '../core/shared/operators'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; export const SEARCH_ROUTE = '/search'; @@ -114,16 +114,16 @@ export class SearchPageComponent implements OnInit { this.searchLink = this.getSearchLink(); this.searchOptions$ = this.getSearchOptions(); this.sub = this.searchOptions$.pipe( - switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(observableOf(undefined))))) + switchMap((options) => this.service.search(options).pipe( + getSucceededRemoteData(), + startWith(undefined) + ))) .subscribe((results) => { this.resultsRD$.next(results); }); this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe( switchMap((scopeId) => this.service.getScopes(scopeId)) ); - if (!isNotEmpty(this.fixedFilter$)) { - this.fixedFilter$ = this.routeService.getRouteParameterValue('filter'); - } } /** diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts index 14fcdd8d60..fa43b27e66 100644 --- a/src/app/+search-page/search-service/search-configuration.service.ts +++ b/src/app/+search-page/search-service/search-configuration.service.ts @@ -14,7 +14,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SearchOptions } from '../search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteData } from '../../core/data/remote-data'; import { getSucceededRemoteData } from '../../core/shared/operators'; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 9ec5bc35f2..b505504870 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -26,7 +26,7 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { map } from 'rxjs/operators'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; import { routeServiceStub } from '../../shared/testing/route-service-stub'; @Component({ template: '' }) diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 52be0417a8..6685e7b715 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -42,7 +42,7 @@ 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 { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../core/services/route.service'; /** * Service that performs all general actions that have to do with the search page diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index bd2d832c67..b7b34f9c55 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -26,7 +26,7 @@ import { HostWindowResizeAction } from './shared/host-window.actions'; import { MetadataService } from './core/metadata/metadata.service'; import { GLOBAL_CONFIG, ENV_CONFIG } from '../config'; -import { NativeWindowRef, NativeWindowService } from './shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { MockTranslateLoader } from './shared/mocks/mock-translate-loader'; import { MockMetadataService } from './shared/mocks/mock-metadata-service'; @@ -41,7 +41,7 @@ import { MenuServiceStub } from './shared/testing/menu-service-stub'; import { HostWindowService } from './shared/host-window.service'; import { HostWindowServiceStub } from './shared/testing/host-window-service-stub'; import { ActivatedRoute, Router } from '@angular/router'; -import { RouteService } from './shared/services/route.service'; +import { RouteService } from './core/services/route.service'; import { MockActivatedRoute } from './shared/mocks/mock-active-router'; import { MockRouter } from './shared/mocks/mock-router'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 37cc791558..836c20208d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,11 +19,11 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config'; import { MetadataService } from './core/metadata/metadata.service'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowState } from './shared/host-window.reducer'; -import { NativeWindowRef, NativeWindowService } from './shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from './core/services/window.service'; import { isAuthenticated } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; -import { RouteService } from './shared/services/route.service'; +import { RouteService } from './core/services/route.service'; import variables from '../styles/_exposed_variables.scss'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.service'; diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index e766a45e48..ab2e6fd86b 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -7,12 +7,12 @@ import { REQUEST } from '@nguniversal/express-engine/tokens'; import { of as observableOf } from 'rxjs'; import { authReducer, AuthState } from './auth.reducer'; -import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { AuthService } from './auth.service'; import { RouterStub } from '../../shared/testing/router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; -import { CookieService } from '../../shared/services/cookie.service'; +import { CookieService } from '../services/cookie.service'; import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service-stub'; import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; @@ -20,7 +20,7 @@ import { AuthTokenInfo } from './models/auth-token-info.model'; import { EPerson } from '../eperson/models/eperson.model'; import { EPersonMock } from '../../shared/testing/eperson-mock'; import { AppState } from '../../app.reducer'; -import { ClientCookieService } from '../../shared/services/client-cookie.service'; +import { ClientCookieService } from '../services/client-cookie.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a01768e687..08c94b02f2 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -15,11 +15,11 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; -import { CookieService } from '../../shared/services/cookie.service'; +import { CookieService } from '../services/cookie.service'; import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; -import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 9ade23e6c5..f657b5d449 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -6,7 +6,7 @@ import { AuthEffects } from './auth/auth.effects'; import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; -import { RouteEffects } from '../shared/services/route.effects'; +import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 6550435aa3..31d1da1ede 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -15,7 +15,7 @@ import { coreReducers } from './core.reducers'; import { isNotEmpty } from '../shared/empty.util'; -import { ApiService } from '../shared/services/api.service'; +import { ApiService } from './services/api.service'; import { BrowseEntriesResponseParsingService } from './data/browse-entries-response-parsing.service'; import { CollectionDataService } from './data/collection-data.service'; import { CommunityDataService } from './data/community-data.service'; @@ -35,12 +35,12 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RequestService } from './data/request.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { ServerResponseService } from '../shared/services/server-response.service'; -import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service'; +import { ServerResponseService } from './services/server-response.service'; +import { NativeWindowFactory, NativeWindowService } from './services/window.service'; import { BrowseService } from './browse/browse.service'; import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; import { ConfigResponseParsingService } from './config/config-response-parsing.service'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from './services/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 7aecb91a7a..4fcf36f9cc 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -13,7 +13,7 @@ import { objectUpdatesReducer, ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; -import { routeReducer, RouteState } from '../shared/services/route.reducer'; +import { routeReducer, RouteState } from './services/route.reducer'; export interface CoreState { 'cache/object': ObjectCacheState, diff --git a/src/app/shared/services/api.service.ts b/src/app/core/services/api.service.ts similarity index 100% rename from src/app/shared/services/api.service.ts rename to src/app/core/services/api.service.ts diff --git a/src/app/shared/services/client-cookie.service.ts b/src/app/core/services/client-cookie.service.ts similarity index 100% rename from src/app/shared/services/client-cookie.service.ts rename to src/app/core/services/client-cookie.service.ts diff --git a/src/app/shared/services/cookie.service.spec.ts b/src/app/core/services/cookie.service.spec.ts similarity index 100% rename from src/app/shared/services/cookie.service.spec.ts rename to src/app/core/services/cookie.service.spec.ts diff --git a/src/app/shared/services/cookie.service.ts b/src/app/core/services/cookie.service.ts similarity index 100% rename from src/app/shared/services/cookie.service.ts rename to src/app/core/services/cookie.service.ts diff --git a/src/app/shared/services/route.actions.ts b/src/app/core/services/route.actions.ts similarity index 100% rename from src/app/shared/services/route.actions.ts rename to src/app/core/services/route.actions.ts diff --git a/src/app/shared/services/route.effects.ts b/src/app/core/services/route.effects.ts similarity index 100% rename from src/app/shared/services/route.effects.ts rename to src/app/core/services/route.effects.ts diff --git a/src/app/shared/services/route.reducer.ts b/src/app/core/services/route.reducer.ts similarity index 100% rename from src/app/shared/services/route.reducer.ts rename to src/app/core/services/route.reducer.ts diff --git a/src/app/shared/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts similarity index 97% rename from src/app/shared/services/route.service.spec.ts rename to src/app/core/services/route.service.spec.ts index c6003521a7..ae31f28384 100644 --- a/src/app/shared/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -6,9 +6,9 @@ import { Store } from '@ngrx/store'; import { getTestScheduler, hot } from 'jasmine-marbles'; import { RouteService } from './route.service'; -import { MockRouter } from '../mocks/mock-router'; +import { MockRouter } from '../../shared/mocks/mock-router'; import { TestScheduler } from 'rxjs/testing'; -import { AddUrlToHistoryAction } from '../history/history.actions'; +import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; describe('RouteService', () => { let scheduler: TestScheduler; diff --git a/src/app/shared/services/route.service.ts b/src/app/core/services/route.service.ts similarity index 95% rename from src/app/shared/services/route.service.ts rename to src/app/core/services/route.service.ts index dc626484c1..65aa858945 100644 --- a/src/app/shared/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -12,12 +12,12 @@ import { combineLatest, Observable } from 'rxjs'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { isEqual } from 'lodash'; -import { AddUrlToHistoryAction } from '../history/history.actions'; -import { historySelector } from '../history/selectors'; +import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; +import { historySelector } from '../../shared/history/selectors'; import { SetParametersAction, SetQueryParametersAction } from './route.actions'; -import { CoreState } from '../../core/core.reducers'; -import { hasValue } from '../empty.util'; -import { coreSelector } from '../../core/core.selectors'; +import { CoreState } from '../core.reducers'; +import { hasValue } from '../../shared/empty.util'; +import { coreSelector } from '../core.selectors'; /** * Selector to select all route parameters from the store diff --git a/src/app/shared/services/server-cookie.service.ts b/src/app/core/services/server-cookie.service.ts similarity index 100% rename from src/app/shared/services/server-cookie.service.ts rename to src/app/core/services/server-cookie.service.ts diff --git a/src/app/shared/services/server-response.service.ts b/src/app/core/services/server-response.service.ts similarity index 100% rename from src/app/shared/services/server-response.service.ts rename to src/app/core/services/server-response.service.ts diff --git a/src/app/shared/services/window.service.ts b/src/app/core/services/window.service.ts similarity index 100% rename from src/app/shared/services/window.service.ts rename to src/app/core/services/window.service.ts diff --git a/src/app/pagenotfound/pagenotfound.component.ts b/src/app/pagenotfound/pagenotfound.component.ts index 6e173b4139..b11de58269 100644 --- a/src/app/pagenotfound/pagenotfound.component.ts +++ b/src/app/pagenotfound/pagenotfound.component.ts @@ -1,4 +1,4 @@ -import { ServerResponseService } from '../shared/services/server-response.service'; +import { ServerResponseService } from '../core/services/server-response.service'; import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; import { AuthService } from '../core/auth/auth.service'; diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 4dad4a703f..c53c45fbe9 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -1,6 +1,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CommunityDataService } from '../../../core/data/community-data.service'; -import { RouteService } from '../../services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index c9fcfecb97..e07f2a5a0a 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Community } from '../../../core/shared/community.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { Observable } from 'rxjs'; -import { RouteService } from '../../services/route.service'; +import { RouteService } from '../../../core/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../../core/data/remote-data'; import { isNotEmpty, isNotUndefined } from '../../empty.util'; diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index e6c24226e2..82f57ea970 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -31,7 +31,7 @@ import { SubmissionSubmitComponent } from './submit/submission-submit.component' @NgModule({ imports: [ CommonModule, - CoreModule, + CoreModule.forRoot(), SharedModule, StoreModule.forFeature('submission', submissionReducers, {}), EffectsModule.forFeature(submissionEffects), diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index d764f09538..80ad3b606a 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -12,7 +12,7 @@ import { MockRouter } from '../shared/mocks/mock-router'; import { SubmissionService } from './submission.service'; import { submissionReducers } from './submission.reducers'; import { SubmissionRestService } from '../core/submission/submission-rest.service'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { SubmissionRestServiceStub } from '../shared/testing/submission-rest-service-stub'; import { MockActivatedRoute } from '../shared/mocks/mock-active-router'; import { GLOBAL_CONFIG } from '../../config'; diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 82185a8eae..36aedaaab6 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -35,7 +35,7 @@ import { SubmissionRestService } from '../core/submission/submission-rest.servic import { SectionDataObject } from './sections/models/section-data.model'; import { SubmissionScopeType } from '../core/submission/submission-scope-type'; import { SubmissionObject } from '../core/submission/models/submission-object.model'; -import { RouteService } from '../shared/services/route.service'; +import { RouteService } from '../core/services/route.service'; import { SectionsType } from './sections/sections-type'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { SubmissionDefinitionsModel } from '../core/config/models/config-submission-definitions.model'; diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index b20894880b..7ff70457bb 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -15,8 +15,8 @@ import { AppComponent } from '../../app/app.component'; import { AppModule } from '../../app/app.module'; import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module'; import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; -import { ClientCookieService } from '../../app/shared/services/client-cookie.service'; -import { CookieService } from '../../app/shared/services/cookie.service'; +import { ClientCookieService } from '../../app/core/services/client-cookie.service'; +import { CookieService } from '../../app/core/services/cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { Angulartics2Module } from 'angulartics2'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index d809d3cced..bd3379c8de 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -13,8 +13,8 @@ import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service'; import { TranslateUniversalLoader } from '../translate-universal-loader'; -import { CookieService } from '../../app/shared/services/cookie.service'; -import { ServerCookieService } from '../../app/shared/services/server-cookie.service'; +import { CookieService } from '../../app/core/services/cookie.service'; +import { ServerCookieService } from '../../app/core/services/server-cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { ServerAuthService } from '../../app/core/auth/server-auth.service'; From 14d7437da927d6087a98721660e9800e23c1f4f4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 26 Jun 2019 17:47:44 +0200 Subject: [PATCH 039/149] 63184: Edit-Relationships left/right Item refactoring --- .../item-relationships.component.ts | 68 +++++++------ .../shared/item-relationships-utils.ts | 17 +++- src/app/core/data/relationship.service.ts | 98 +++++++++++++++---- 3 files changed, 133 insertions(+), 50 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 3e74794866..36b2e212ba 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -21,6 +21,7 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; import { Subscription } from 'rxjs/internal/Subscription'; +import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils'; @Component({ selector: 'ds-item-relationships', @@ -94,7 +95,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl } /** - * Resolve the currently selected related items back to relationships and send a delete request + * Resolve the currently selected related items back to relationships and send a delete request for each of the relationships found * Make sure the lists are refreshed afterwards and notifications are sent for success and errors */ public submit(): void { @@ -105,42 +106,51 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field.uuid) as string[]), isNotEmptyOperator() ); - const allRelationshipsAndRemovedItemIds$ = observableCombineLatest( - this.relationshipService.getItemRelationshipsArray(this.item), - removedItemIds$ - ); - // Get all IDs of the relationships that should be removed - const removedRelationshipIds$ = allRelationshipsAndRemovedItemIds$.pipe( - map(([relationships, itemIds]) => - relationships - .filter((relationship: Relationship) => itemIds.indexOf(relationship.leftId) > -1 || itemIds.indexOf(relationship.rightId) > -1) - .map((relationship: Relationship) => relationship.id)) + // Get all the relationships that should be removed + const removedRelationships$ = removedItemIds$.pipe( + getRelationsByRelatedItemIds(this.item, this.relationshipService) ); // Request a delete for every relationship found in the observable created above - removedRelationshipIds$.pipe( + removedRelationships$.pipe( take(1), + map((removedRelationships: Relationship[]) => removedRelationships.map((rel: Relationship) => rel.id)), switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid)))) ).subscribe((responses: RestResponse[]) => { - const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); - const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful); - - // Display an error notification for each failed request - failedResponses.forEach((response: ErrorResponse) => { - this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); - }); - if (successfulResponses.length > 0) { - // Remove the item's cache to make sure the lists are reloaded with the newest values - this.objectCache.remove(this.item.self); - this.requestService.removeByHrefSubstring(this.item.self); - // Send a notification that the removal was successful - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); - } - // Reset the state of editing relationships - this.initializeOriginalFields(); - this.initializeUpdates(); + this.displayNotifications(responses); + this.reset(); }); } + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param responses + */ + displayNotifications(responses: RestResponse[]) { + const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); + const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful); + + failedResponses.forEach((response: ErrorResponse) => { + this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); + }); + if (successfulResponses.length > 0) { + // Remove the item's cache to make sure the lists are reloaded with the newest values + this.objectCache.remove(this.item.self); + this.requestService.removeByHrefSubstring(this.item.self); + // Send a notification that the removal was successful + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + } + } + + /** + * Reset the state of editing relationships + */ + reset() { + this.initializeOriginalFields(); + this.initializeUpdates(); + } + /** * Sends all initial values of this item to the object updates service */ 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 91f7c52bb8..eaea3d5d3e 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 @@ -7,11 +7,12 @@ import { hasValue } from '../../../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; -import { distinctUntilChanged, flatMap, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, flatMap, map, tap } from 'rxjs/operators'; import { of as observableOf, zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs'; import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { RemoteData } from '../../../../core/data/remote-data'; +import { RelationshipService } from '../../../../core/data/relationship.service'; /** * Operator for comparing arrays using a mapping function @@ -120,3 +121,17 @@ export const relationsToRepresentations = (parentId: string, itemType: string, m ) ) ); + +/** + * Operator for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup) + * Only relationships where leftItem or rightItem's ID is present in the list provided will be returned + * @param item + * @param relationshipService + */ +export const getRelationsByRelatedItemIds = (item: Item, relationshipService: RelationshipService) => + (source: Observable): Observable => + source.pipe( + flatMap((relatedItemIds: string[]) => relationshipService.getItemResolvedRelatedItemsAndRelationships(item).pipe( + map(([leftItems, rightItems, rels]) => rels.filter((rel: Relationship, index: number) => relatedItemIds.indexOf(leftItems[index].uuid) > -1 || relatedItemIds.indexOf(rightItems[index].uuid) > -1)) + )) + ); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 000faaf2c3..fca5074a88 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -3,7 +3,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, @@ -70,20 +70,35 @@ export class RelationshipService { * @param item */ getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> { - const relationships$ = this.getItemRelationshipsArray(item); - - const relationshipTypes$ = relationships$.pipe( - flatMap((rels: Relationship[]) => - observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe( - map(([...arr]: Array>) => arr.map((d: RemoteData) => d.payload).filter((type) => hasValue(type))) - ) - ), - distinctUntilChanged(compareArraysUsingIds()) - ); - return observableCombineLatest( - relationships$, - relationshipTypes$ + this.getItemRelationshipsArray(item), + this.getItemRelationshipTypesArray(item) + ); + } + + /** + * Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships their types + * This is used for easier access of a relationship's type and left and right items because they exist as observables + * @param item + */ + getItemResolvedRelatedItemsAndTypes(item: Item): Observable<[Item[], Item[], RelationshipType[]]> { + return observableCombineLatest( + this.getItemLeftRelatedItemArray(item), + this.getItemRightRelatedItemArray(item), + this.getItemRelationshipTypesArray(item) + ); + } + + /** + * Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships themselves + * This is used for easier access of the relationship and their left and right items because they exist as observables + * @param item + */ + getItemResolvedRelatedItemsAndRelationships(item: Item): Observable<[Item[], Item[], Relationship[]]> { + return observableCombineLatest( + this.getItemLeftRelatedItemArray(item), + this.getItemRightRelatedItemArray(item), + this.getItemRelationshipsArray(item) ); } @@ -101,17 +116,60 @@ export class RelationshipService { ); } + /** + * Get an item their relationship types in the form of an array + * @param item + */ + getItemRelationshipTypesArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => + observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe( + map(([...arr]: Array>) => arr.map((d: RemoteData) => d.payload).filter((type) => hasValue(type))), + filter((arr) => arr.length === rels.length) + ) + ), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an item his relationship's left-side related items in the form of an array + * @param item + */ + getItemLeftRelatedItemArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.leftItem)).pipe( + map(([...arr]: Array>) => arr.map((rd: RemoteData) => rd.payload).filter((i) => hasValue(i))), + filter((arr) => arr.length === rels.length) + )), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + + /** + * Get an item his relationship's right-side related items in the form of an array + * @param item + */ + getItemRightRelatedItemArray(item: Item): Observable { + return this.getItemRelationshipsArray(item).pipe( + flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.rightItem)).pipe( + map(([...arr]: Array>) => arr.map((rd: RemoteData) => rd.payload).filter((i) => hasValue(i))), + filter((arr) => arr.length === rels.length) + )), + distinctUntilChanged(compareArraysUsingIds()) + ); + } + /** * Get an array of an item their unique relationship type's labels * The array doesn't contain any duplicate labels * @param item */ getItemRelationshipLabels(item: Item): Observable { - return this.getItemResolvedRelsAndTypes(item).pipe( - map(([relsCurrentPage, relTypesCurrentPage]) => { + return this.getItemResolvedRelatedItemsAndTypes(item).pipe( + map(([leftItems, rightItems, relTypesCurrentPage]) => { return relTypesCurrentPage.map((type, index) => { - const relationship = relsCurrentPage[index]; - if (relationship.leftId === item.uuid) { + if (leftItems[index].uuid === item.uuid) { return type.leftLabel; } else { return type.rightLabel; @@ -128,7 +186,7 @@ export class RelationshipService { */ getRelatedItems(item: Item): Observable { return this.getItemRelationshipsArray(item).pipe( - relationsToItems(item.uuid, this.itemService) + relationsToItems(item.uuid) ); } @@ -141,7 +199,7 @@ export class RelationshipService { getRelatedItemsByLabel(item: Item, label: string): Observable { return this.getItemResolvedRelsAndTypes(item).pipe( filterRelationsByTypeLabel(label), - relationsToItems(item.uuid, this.itemService) + relationsToItems(item.uuid) ); } From 2f6c0fb1264da0a2c4c5791ba06dbaab8230e5d1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Jun 2019 10:19:53 +0200 Subject: [PATCH 040/149] 63184: Fixed test cases --- .../item-relationships.component.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 2439eb4c63..1d3d59201b 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 @@ -15,7 +15,7 @@ import { GLOBAL_CONFIG } from '../../../../config'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { ResourceType } from '../../../core/shared/resource-type'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { PaginatedList } from '../../../core/data/paginated-list'; @@ -78,16 +78,12 @@ describe('ItemRelationshipsComponent', () => { self: url + '/2', id: '2', uuid: '2', - leftId: 'author1', - rightId: 'publication', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) }), Object.assign(new Relationship(), { self: url + '/3', id: '3', uuid: '3', - leftId: 'author2', - rightId: 'publication', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) }) ]; @@ -109,6 +105,11 @@ describe('ItemRelationshipsComponent', () => { uuid: 'author2' }); + relationships[0].leftItem = observableOf(new RemoteData(false, false, true, undefined, author1)); + relationships[0].rightItem = observableOf(new RemoteData(false, false, true, undefined, item)); + relationships[1].leftItem = observableOf(new RemoteData(false, false, true, undefined, author2)); + relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item)); + fieldUpdate1 = { field: author1, changeType: undefined @@ -155,7 +156,8 @@ describe('ItemRelationshipsComponent', () => { getRelatedItems: observableOf([author1, author2]), getRelatedItemsByLabel: observableOf([author1, author2]), getItemRelationshipsArray: observableOf(relationships), - deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')) + deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')), + getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships)) } ); From 417f79ab6a8108f6f18745cf25cbff3c8625d1db Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 27 Jun 2019 11:55:10 +0200 Subject: [PATCH 041/149] 63184: Relationship list refreshing fix - Re-initializing itemUpdateSubscription --- .../item-relationships/item-relationships.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 36b2e212ba..087dea656b 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -66,8 +66,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl ngOnInit(): void { super.ngOnInit(); this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item); + this.initializeItemUpdate(); + } - // Update the item (and view) when it's removed in the request cache + /** + * Update the item (and view) when it's removed in the request cache + */ + public initializeItemUpdate(): void { this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( filter((exists: boolean) => !exists), switchMap(() => this.itemService.findById(this.item.uuid)), @@ -144,11 +149,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl } /** - * Reset the state of editing relationships + * Re-initialize fields and subscriptions */ reset() { this.initializeOriginalFields(); this.initializeUpdates(); + this.initializeItemUpdate(); } /** From d734ed108ef960283672ac27dd0a0fb7f2edf946 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 4 Jul 2019 17:52:14 +0200 Subject: [PATCH 042/149] 63469: Intermediate commit --- .../item-relationships.component.ts | 4 --- src/app/core/data/relationship.service.ts | 25 +++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index 087dea656b..e8f34bc70e 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -140,10 +140,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); }); if (successfulResponses.length > 0) { - // Remove the item's cache to make sure the lists are reloaded with the newest values - this.objectCache.remove(this.item.self); - this.requestService.removeByHrefSubstring(this.item.self); - // Send a notification that the removal was successful this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); } } diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index fca5074a88..735674afc3 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -25,6 +25,7 @@ import { compareArraysUsingIds, filterRelationsByTypeLabel, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { ObjectCacheService } from '../cache/object-cache.service'; /** * The service handling all relationship requests @@ -36,7 +37,8 @@ export class RelationshipService { constructor(protected requestService: RequestService, protected halService: HALEndpointService, protected rdbService: RemoteDataBuildService, - protected itemService: ItemDataService) { + protected itemService: ItemDataService, + protected objectCache: ObjectCacheService) { } /** @@ -49,6 +51,11 @@ export class RelationshipService { ); } + findById(uuid: string): Observable> { + const href$ = this.getRelationshipEndpoint(uuid); + return this.rdbService.buildSingle(href$); + } + /** * Send a delete request for a relationship by ID * @param uuid @@ -60,7 +67,8 @@ export class RelationshipService { map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), configureRequest(this.requestService), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), - getResponseFromEntry() + getResponseFromEntry(), + tap(() => this.clearRelatedCache(uuid)) ); } @@ -203,4 +211,17 @@ export class RelationshipService { ); } + clearRelatedCache(uuid: string) { + this.findById(uuid).pipe( + getSucceededRemoteData(), + flatMap((rd: RemoteData) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))), + take(1) + ).subscribe(([leftItem, rightItem]) => { + this.objectCache.remove(leftItem.payload.self); + this.objectCache.remove(rightItem.payload.self); + this.requestService.removeByHrefSubstring(leftItem.payload.self); + this.requestService.removeByHrefSubstring(rightItem.payload.self); + }); + } + } From 8265942f18349bd8c6cb4d358f03a7209c9c968f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Jul 2019 13:41:20 +0200 Subject: [PATCH 043/149] 63469: Properly reload de-cached items --- .../+search-page/search-service/search.service.ts | 13 +++++++------ src/app/core/shared/operators.ts | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 598657a1b2..79164b50c3 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,7 +1,7 @@ -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs'; import { Injectable, OnDestroy } from '@angular/core'; import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; -import { first, map, switchMap } from 'rxjs/operators'; +import { first, map, switchMap, tap } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { FacetConfigSuccessResponse, @@ -23,7 +23,7 @@ import { getSucceededRemoteData } from '../../core/shared/operators'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { NormalizedSearchResult } from '../normalized-search-result.model'; import { SearchOptions } from '../search-options.model'; import { SearchResult } from '../search-result.model'; @@ -137,10 +137,11 @@ export class SearchService implements OnDestroy { map((sqr: SearchQueryResponse) => { return sqr.objects .filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject)) - .map((nsr: NormalizedSearchResult) => { - return this.rdb.buildSingle(nsr.indexableObject); - }) + .map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), nsr.indexableObject)) }), + // Send a request for each item to ensure fresh cache + tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))), + map((reqs: RestRequest[]) => reqs.map((req: RestRequest) => this.rdb.buildSingle(req.href))), switchMap((input: Array>>) => this.rdb.aggregate(input)), ); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ae46691e39..d46c688e68 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -91,7 +91,7 @@ export const toDSpaceObjectListRD = () => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { - const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.indexableObject); + const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult) => searchResult.indexableObject); const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList; return Object.assign(rd, { payload: payload }); }) From 34e75a46e5919f4aaa436e5c5c49b9b6a8cfc889 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 8 Jul 2019 14:30:19 +0200 Subject: [PATCH 044/149] 63469: JSDocs + tests --- .../item-relationships.component.spec.ts | 4 +- .../core/data/relationship.service.spec.ts | 61 +++++++++++++------ src/app/core/data/relationship.service.ts | 8 +++ 3 files changed, 50 insertions(+), 23 deletions(-) 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 1d3d59201b..51394ef9e5 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 @@ -227,10 +227,8 @@ describe('ItemRelationshipsComponent', () => { comp.submit(); }); - it('it should delete the correct relationship and de-cache the current item', () => { + it('it should delete the correct relationship', () => { expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid); - expect(objectCache.remove).toHaveBeenCalledWith(item.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); }); }); }); diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index ce2b169eef..88da4a5496 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -13,6 +13,8 @@ import { Item } from '../shared/item.model'; import { PaginatedList } from './paginated-list'; import { PageInfo } from '../shared/page-info.model'; import { DeleteRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Observable } from 'rxjs/internal/Observable'; describe('RelationshipService', () => { let service: RelationshipService; @@ -22,6 +24,11 @@ describe('RelationshipService', () => { const relationshipsEndpointURL = `${restEndpointURL}/relationships`; const halService: any = new HALEndpointServiceStub(restEndpointURL); const rdbService = getMockRemoteDataBuildService(); + const objectCache = Object.assign({ + /* tslint:disable:no-empty */ + remove: () => {} + /* tslint:enable:no-empty */ + }) as ObjectCacheService; const relationshipType = Object.assign(new RelationshipType(), { type: ResourceType.RelationshipType, @@ -31,24 +38,20 @@ describe('RelationshipService', () => { rightLabel: 'isPublicationOfAuthor' }); - const relationships = [ - Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/2', - id: '2', - uuid: '2', - leftId: 'author1', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }), - Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/3', - id: '3', - uuid: '3', - leftId: 'author2', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }) - ]; + const relationship1 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/2', + id: '2', + uuid: '2', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + const relationship2 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/3', + id: '3', + uuid: '3', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + + const relationships = [ relationship1, relationship2 ]; const item = Object.assign(new Item(), { self: 'fake-item-url/publication', @@ -65,6 +68,10 @@ describe('RelationshipService', () => { id: 'author2', uuid: 'author2' }); + relationship1.leftItem = getRemotedataObservable(relatedItem1); + relationship1.rightItem = getRemotedataObservable(item); + relationship2.leftItem = getRemotedataObservable(relatedItem2); + relationship2.rightItem = getRemotedataObservable(item); const relatedItems = [relatedItem1, relatedItem2]; const itemService = jasmine.createSpyObj('itemService', { @@ -76,7 +83,8 @@ describe('RelationshipService', () => { requestService, halService, rdbService, - itemService + itemService, + objectCache ); } @@ -93,13 +101,22 @@ describe('RelationshipService', () => { describe('deleteRelationship', () => { beforeEach(() => { + spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1)); + spyOn(objectCache, 'remove'); service.deleteRelationship(relationships[0].uuid).subscribe(); }); it('should send a DeleteRequest', () => { - const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationships[0].uuid); + const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid); expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); }); + + it('should clear the related items their cache', () => { + expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); + expect(objectCache.remove).toHaveBeenCalledWith(item.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); + }); }); describe('getItemRelationshipsArray', () => { @@ -135,3 +152,7 @@ describe('RelationshipService', () => { }) }); + +function getRemotedataObservable(obj: any): Observable> { + return observableOf(new RemoteData(false, false, true, undefined, obj)); +} diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 735674afc3..1699b6a27d 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -51,6 +51,10 @@ export class RelationshipService { ); } + /** + * Find a relationship by its UUID + * @param uuid + */ findById(uuid: string): Observable> { const href$ = this.getRelationshipEndpoint(uuid); return this.rdbService.buildSingle(href$); @@ -211,6 +215,10 @@ export class RelationshipService { ); } + /** + * Clear object and request caches of the items related to a relationship (left and right items) + * @param uuid + */ clearRelatedCache(uuid: string) { this.findById(uuid).pipe( getSucceededRemoteData(), From ed959e492a27213fc97b11a986c5778b85addbd2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 4 Jul 2019 11:17:56 +0200 Subject: [PATCH 045/149] 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 046/149] 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 047/149] 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 048/149] 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 049/149] 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 050/149] 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 051/149] 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 052/149] 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 bd8177c17d88e55f7f120855e67f3cfbd7f6f4a7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Jun 2019 17:46:45 +0200 Subject: [PATCH 053/149] 62741: AoT build fix --- .../object-list/item-type-badge/item-type-badge.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts index 53e36a535d..9ffba33758 100644 --- a/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts @@ -1,10 +1,12 @@ import { Component, Input } from '@angular/core'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; +import { SearchResult } from '../../../+search-page/search-result.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; @Component({ selector: 'ds-item-type-badge', templateUrl: './item-type-badge.component.html' }) export class ItemTypeBadgeComponent { - @Input() object: ListableObject; + @Input() object: SearchResult; } From c2345c1562e1af83a506330c4c48ca7717f1cd27 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 18 Jul 2019 11:51:46 +0200 Subject: [PATCH 054/149] 63825: UI language cookie --- src/app/app.component.spec.ts | 3 ++ src/app/app.component.ts | 22 ++++++++-- src/app/app.module.ts | 4 +- .../lang-switch/lang-switch.component.html | 2 +- .../lang-switch/lang-switch.component.spec.ts | 40 ++++++++++++++++++- .../lang-switch/lang-switch.component.ts | 14 ++++++- .../mocks/mock-client-cookie.service.ts | 26 ++++++++++++ 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 src/app/shared/mocks/mock-client-cookie.service.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index bd2d832c67..016d8df8d4 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -44,6 +44,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { RouteService } from './shared/services/route.service'; import { MockActivatedRoute } from './shared/mocks/mock-active-router'; import { MockRouter } from './shared/mocks/mock-router'; +import { ClientCookieService } from './shared/services/client-cookie.service'; +import { MockClientCookieService } from './shared/mocks/mock-client-cookie.service'; let comp: AppComponent; let fixture: ComponentFixture; @@ -78,6 +80,7 @@ describe('App component', () => { { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: ClientCookieService, useValue: new MockClientCookieService()}, AppComponent, RouteService ], diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 52c169e7bc..cc71247d9f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -32,6 +32,10 @@ import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { slideSidebarPadding } from './shared/animations/slide'; import { HostWindowService } from './shared/host-window.service'; import { Theme } from '../config/theme.inferface'; +import { ClientCookieService } from './shared/services/client-cookie.service'; +import { isNotEmpty } from './shared/empty.util'; + +export const LANG_COOKIE = 'language_cookie'; @Component({ selector: 'ds-app', @@ -61,6 +65,7 @@ export class AppComponent implements OnInit, AfterViewInit { private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService, + private clientCookie: ClientCookieService ) { // Load all the languages that are defined as active from the config file translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); @@ -68,11 +73,20 @@ export class AppComponent implements OnInit, AfterViewInit { // Load the default language from the config file translate.setDefaultLang(config.defaultLanguage); - // Attempt to get the browser language from the user - if (translate.getLangs().includes(translate.getBrowserLang())) { - translate.use(translate.getBrowserLang()); + // Attempt to get the language from a cookie + const lang = clientCookie.get(LANG_COOKIE); + if (isNotEmpty(lang)) { + // Cookie found + // Use the language from the cookie + translate.use(lang); } else { - translate.use(config.defaultLanguage); + // Cookie not found + // Attempt to get the browser language from the user + if (translate.getLangs().includes(translate.getBrowserLang())) { + translate.use(translate.getBrowserLang()); + } else { + translate.use(config.defaultLanguage); + } } metadata.listenForRouteChange(); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ce5a2d78a2..3781edf532 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -39,6 +39,7 @@ import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/e import { NavbarModule } from './navbar/navbar.module'; import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module'; +import { ClientCookieService } from './shared/services/client-cookie.service'; export function getConfig() { return ENV_CONFIG; @@ -97,7 +98,8 @@ const PROVIDERS = [ { provide: RouterStateSerializer, useClass: DSpaceRouterStateSerializer - } + }, + ClientCookieService ]; const DECLARATIONS = [ diff --git a/src/app/shared/lang-switch/lang-switch.component.html b/src/app/shared/lang-switch/lang-switch.component.html index 745facc95c..b61ec5592e 100644 --- a/src/app/shared/lang-switch/lang-switch.component.html +++ b/src/app/shared/lang-switch/lang-switch.component.html @@ -4,7 +4,7 @@
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 772240eb0b..6402d07f9b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -21,9 +21,9 @@ import { SearchService } from '../../../search-service/search.service'; import { FILTER_CONFIG, IN_PLACE_SEARCH, SearchFilterService } from '../search-filter.service'; import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; -import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { SearchOptions } from '../../../search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component'; +import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; @Component({ selector: 'ds-search-facet-filter', diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index ac2a72f4b6..027f162a0a 100644 --- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -15,7 +15,7 @@ | translate}}
- + >
diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html index a4f4fb5ee8..5241c6c0bd 100644 --- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html @@ -15,7 +15,7 @@ | translate}}
- + ngDefaultControl>
diff --git a/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html new file mode 100644 index 0000000000..016ff8c06c --- /dev/null +++ b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts new file mode 100644 index 0000000000..2e343a6834 --- /dev/null +++ b/src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.spec.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DsoInputSuggestionsComponent } from './dso-input-suggestions.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +describe('DsoInputSuggestionsComponent', () => { + + let comp: DsoInputSuggestionsComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + + const dso1 = { + uuid: 'test-uuid-1', + name: 'test-name-1' + } as DSpaceObject; + + const dso2 = { + uuid: 'test-uuid-2', + name: 'test-name-2' + } as DSpaceObject; + + const dso3 = { + uuid: 'test-uuid-3', + name: 'test-name-3' + } as DSpaceObject; + + const suggestions = [dso1, dso2, dso3]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule], + declarations: [DsoInputSuggestionsComponent], + providers: [], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(DsoInputSuggestionsComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoInputSuggestionsComponent); + + comp = fixture.componentInstance; // LoadingComponent test instance + comp.suggestions = suggestions; + // query for the message