From 8188da0c7587ad15b59488cd86395c77d2b0d0cd Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 Jul 2019 13:24:24 +0200 Subject: [PATCH 01/77] 63669: Intermediate commit --- .../collection-source.component.html | 7 +++++ .../collection-source.component.ts | 28 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index e69de29bb2..5837a9ed72 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -0,0 +1,7 @@ +

Content source

+
+ + +
+

Configure an external source

+ diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 6ec5be884d..7e167805c1 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; +import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; /** * Component for managing the content source of the collection @@ -7,6 +9,28 @@ import { Component } from '@angular/core'; selector: 'ds-collection-source', templateUrl: './collection-source.component.html', }) -export class CollectionSourceComponent { - /* TODO: Implement Collection Edit - Content Source */ +export class CollectionSourceComponent extends AbstractTrackableComponent { + externalSource = false; + + /** + * The dynamic form fields used for creating/editing a collection + * @type {(DynamicInputModel | DynamicTextAreaModel)[]} + */ + formModel: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'title', + name: 'dc.title', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'Please enter a name for this title' + }, + }), + new DynamicTextAreaModel({ + id: 'provider', + name: 'provider', + }) + ]; } From 03887e7c41d8e23e11b2f54734b982a08af13f77 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 Jul 2019 17:57:54 +0200 Subject: [PATCH 02/77] 63669: Intermediate commit --- resources/i18n/en.json | 15 +- .../collection-source.component.html | 21 +- .../collection-source.component.ts | 250 ++++++++++++++++-- src/app/core/shared/content-source.model.ts | 19 ++ 4 files changed, 280 insertions(+), 25 deletions(-) create mode 100644 src/app/core/shared/content-source.model.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 76e925f1a2..2aeac293e4 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -42,7 +42,20 @@ }, "source": { "title": "Collection Edit - Content Source", - "head": "Content Source" + "head": "Content Source", + "external": "This collection harvests its content from an external source", + "form": { + "head": "Configure an external source", + "provider": "OAI Provider", + "set": "OAI specific set id", + "format": "Metadata Format", + "harvest": "Content being harvested", + "errors": { + "provider": { + "required": "You must provide a set id of the target collection." + } + } + } }, "curate": { "title": "Collection Edit - Curate", diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index 5837a9ed72..51a652056b 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -1,7 +1,16 @@ -

Content source

-
- - +
+

{{ 'collection.edit.tabs.source.head' | translate }}

+
+ + +
+

{{ 'collection.edit.tabs.source.form.head' | translate }}

-

Configure an external source

- + diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 7e167805c1..cdff87c6ac 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,6 +1,23 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; -import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, + DynamicInputModel, DynamicRadioGroupModel, + DynamicSelectModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { FormGroup } from '@angular/forms'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { ContentSource } from '../../../core/shared/content-source.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Collection } from '../../../core/shared/collection.model'; +import { first, map } from 'rxjs/operators'; +import { ActivatedRoute } from '@angular/router'; /** * Component for managing the content source of the collection @@ -9,28 +26,225 @@ import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from selector: 'ds-collection-source', templateUrl: './collection-source.component.html', }) -export class CollectionSourceComponent extends AbstractTrackableComponent { - externalSource = false; +export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit { + /** + * The current collection's remote data + */ + collectionRD$: Observable>; /** - * The dynamic form fields used for creating/editing a collection + * The current collection's content source + */ + contentSource: ContentSource; + + /** + * @type {string} Key prefix used to generate form labels + */ + LABEL_KEY_PREFIX = 'collection.edit.tabs.source.form.'; + + /** + * @type {string} Key prefix used to generate form error messages + */ + ERROR_KEY_PREFIX = 'collection.edit.tabs.source.form.errors.'; + + /** + * The dynamic form fields used for editing the content source of a collection * @type {(DynamicInputModel | DynamicTextAreaModel)[]} */ formModel: DynamicFormControlModel[] = [ - new DynamicInputModel({ - id: 'title', - name: 'dc.title', - required: true, - validators: { - required: null - }, - errorMessages: { - required: 'Please enter a name for this title' - }, + new DynamicFormGroupModel({ + id: 'providerContainer', + group: [ + new DynamicInputModel({ + id: 'provider', + name: 'provider', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'You must provide a set id of the target collection.' + }, + disabled: true + }) + ] }), - new DynamicTextAreaModel({ - id: 'provider', - name: 'provider', + new DynamicFormGroupModel({ + id: 'setContainer', + group: [ + new DynamicInputModel({ + id: 'set', + name: 'set', + disabled: true + }), + new DynamicSelectModel({ + id: 'format', + name: 'format', + value: 'dc', + options: [ + { + value: 'dc', + label: 'Simple Dublin Core' + }, + { + value: 'qdc', + label: 'Qualified Dublin Core' + }, + { + value: 'dim', + label: 'DSpace Intermediate Metadata' + } + ], + disabled: true + }) + ] + }), + new DynamicFormGroupModel({ + id: 'harvestContainer', + group: [ + new DynamicRadioGroupModel({ + id: 'harvest', + name: 'harvest', + value: 3, + options: [ + { + value: 1, + label: 'Harvest metadata only.' + }, + { + value: 2, + label: 'Harvest metadata and references to bitstreams (requires ORE support).' + }, + { + value: 3, + label: 'Harvest metadata and bitstreams (requires ORE support).' + } + ] + }) + ] }) ]; + + /** + * Layout used for structuring the form inputs + */ + formLayout: DynamicFormLayout = { + provider: { + grid: { + host: 'col-12 d-inline-block' + } + }, + set: { + grid: { + host: 'col col-sm-6 d-inline-block' + } + }, + format: { + grid: { + host: 'col col-sm-6 d-inline-block' + } + }, + harvest: { + grid: { + host: 'col-12' + } + }, + setContainer: { + grid: { + host: 'row' + } + }, + providerContainer: { + grid: { + host: 'row' + } + }, + harvestContainer: { + grid: { + host: 'row' + } + } + }; + + /** + * The form group of this form + */ + formGroup: FormGroup; + + public constructor(public objectUpdatesService: ObjectUpdatesService, + public notificationsService: NotificationsService, + protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected route: ActivatedRoute) { + super(objectUpdatesService, notificationsService, translate); + } + + ngOnInit(): void { + this.formGroup = this.formService.createFormGroup(this.formModel); + this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); + + /* Create a new ContentSource object - TODO: Update to be fetched from the collection */ + this.contentSource = new ContentSource(); + + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); + } + + /** + * Used the update translations of errors and labels on init and on language change + */ + private updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicFormControlModel) => { + if (fieldModel instanceof DynamicFormGroupModel) { + fieldModel.group.forEach((childModel: DynamicFormControlModel) => { + this.updateFieldTranslation(childModel); + }); + } else { + this.updateFieldTranslation(fieldModel); + } + } + ); + } + + /** + * Update the translations of a DynamicInputModel + * @param fieldModel + */ + private updateFieldTranslation(fieldModel: DynamicFormControlModel) { + fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id); + if (isNotEmpty(fieldModel.validators)) { + fieldModel.errorMessages = {}; + Object.keys(fieldModel.validators).forEach((key) => { + fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + }); + } + } + + onChange(event) { + // TODO: Update ContentSource object and add to field update + console.log(event); + } + + onSubmit() { + // TODO: Fetch field update and send to REST API + console.log('submit'); + } + + onCancel() { + this.location.back(); + } + + changeExternalSource() { + this.contentSource.enabled = !this.contentSource.enabled; + if (this.contentSource.enabled) { + this.formGroup.enable(); + } else { + this.formGroup.disable(); + } + } } diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts new file mode 100644 index 0000000000..10f0986b1b --- /dev/null +++ b/src/app/core/shared/content-source.model.ts @@ -0,0 +1,19 @@ +import { v4 as uuid } from 'uuid'; + +export class ContentSource { + uuid: string; + + enabled = false; + + provider: string; + + set: string; + + format: string; + + harvestType: number; + + constructor() { + this.uuid = uuid(); + } +} From 615a47fe5bbada324184b01f5f7e312b9e1c3de8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 12 Jul 2019 13:34:40 +0200 Subject: [PATCH 03/77] 63669: Content-Source integrated with FieldUpdate --- config/environment.default.js | 5 + resources/i18n/en.json | 14 +++ .../collection-source.component.html | 39 +++++++ .../collection-source.component.ts | 106 ++++++++++++++++-- src/app/core/shared/content-source.model.ts | 22 +++- .../trackable/abstract-trackable.component.ts | 4 +- .../collection-page-config.interface.ts | 7 ++ src/config/global-config.interface.ts | 2 + 8 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 src/config/collection-page-config.interface.ts diff --git a/config/environment.default.js b/config/environment.default.js index 804d80b0f2..ce79909515 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -155,5 +155,10 @@ module.exports = { edit: { undoTimeout: 10000 // 10 seconds } + }, + collection: { + edit: { + undoTimeout: 10000 // 10 seconds + } } }; diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 2aeac293e4..ef688cf661 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -55,6 +55,20 @@ "required": "You must provide a set id of the target collection." } } + }, + "notifications": { + "discarded": { + "title": "Changed discarded", + "content": "Your changes were discarded. To reinstate your changes click the 'Undo' button" + }, + "invalid": { + "title": "Metadata invalid", + "content": "Your changes were not saved. Please make sure all fields are valid before you save." + }, + "saved": { + "title": "Content Source saved", + "content": "Your changes to this collection's content source were saved." + } } }, "curate": { diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index 51a652056b..2017f172c5 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -1,4 +1,22 @@
+
+ + + +

{{ 'collection.edit.tabs.source.head' | translate }}

@@ -11,6 +29,27 @@ [formGroup]="formGroup" [formModel]="formModel" [formLayout]="formLayout" + [displaySubmit]="false" (dfChange)="onChange($event)" (submitForm)="onSubmit()" (cancel)="onCancel()"> +
+
+ + + +
+
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index cdff87c6ac..a9696a23f0 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, @@ -11,13 +11,17 @@ import { TranslateService } from '@ngx-translate/core'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { FormGroup } from '@angular/forms'; -import { isNotEmpty } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { ContentSource } from '../../../core/shared/content-source.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { first, map } from 'rxjs/operators'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { cloneDeep } from 'lodash'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; /** * Component for managing the content source of the collection @@ -26,17 +30,22 @@ import { ActivatedRoute } from '@angular/router'; selector: 'ds-collection-source', templateUrl: './collection-source.component.html', }) -export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit { +export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit, OnDestroy { /** * The current collection's remote data */ collectionRD$: Observable>; /** - * The current collection's content source + * The collection's content source */ contentSource: ContentSource; + /** + * The current update to the content source + */ + update$: Observable; + /** * @type {string} Key prefix used to generate form labels */ @@ -80,7 +89,6 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem new DynamicSelectModel({ id: 'format', name: 'format', - value: 'dc', options: [ { value: 'dc', @@ -105,7 +113,6 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem new DynamicRadioGroupModel({ id: 'harvest', name: 'harvest', - value: 3, options: [ { value: 1, @@ -171,16 +178,29 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ formGroup: FormGroup; + /** + * Subscription to update the current form + */ + updateSub: Subscription; + public constructor(public objectUpdatesService: ObjectUpdatesService, public notificationsService: NotificationsService, protected location: Location, protected formService: DynamicFormService, protected translate: TranslateService, - protected route: ActivatedRoute) { + protected route: ActivatedRoute, + protected router: Router, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,) { super(objectUpdatesService, notificationsService, translate); } ngOnInit(): void { + this.notificationsPrefix = 'collection.edit.tabs.source.notifications.'; + this.discardTimeOut = this.EnvConfig.collection.edit.undoTimeout; + this.url = this.router.url; + if (this.url.indexOf('?') > 0) { + this.url = this.url.substr(0, this.url.indexOf('?')); + } this.formGroup = this.formService.createFormGroup(this.formModel); this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); @@ -192,6 +212,33 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem .subscribe(() => { this.updateFieldTranslations(); }); + + this.initializeOriginalContentSource(); + } + + initializeOriginalContentSource() { + const initialContentSource = cloneDeep(this.contentSource); + this.objectUpdatesService.initialize(this.url, [initialContentSource], new Date()); + this.update$ = this.objectUpdatesService.getFieldUpdates(this.url, [initialContentSource]).pipe( + map((updates: FieldUpdates) => updates[initialContentSource.uuid]) + ); + this.updateSub = this.update$.subscribe((update: FieldUpdate) => { + const field = update.field as ContentSource; + this.formGroup.patchValue({ + providerContainer: { + provider: field.provider + }, + setContainer: { + set: field.set, + format: field.format + }, + harvestContainer: { + harvest: field.harvest + } + }); + this.contentSource = cloneDeep(field); + this.switchEnableForm(); + }); } /** @@ -226,13 +273,14 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem } onChange(event) { - // TODO: Update ContentSource object and add to field update - console.log(event); + this.updateContentSourceField(event.model); + this.saveFieldUpdate(); } onSubmit() { // TODO: Fetch field update and send to REST API - console.log('submit'); + this.initializeOriginalContentSource(); + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); } onCancel() { @@ -241,10 +289,46 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem changeExternalSource() { this.contentSource.enabled = !this.contentSource.enabled; + this.updateContentSource(); + } + + /** + * Enable or disable the form depending on the Content Source's enabled property + */ + switchEnableForm() { if (this.contentSource.enabled) { this.formGroup.enable(); } else { this.formGroup.disable(); } } + + updateContentSource() { + this.formModel.forEach( + (fieldModel: DynamicFormGroupModel | DynamicInputModel) => { + if (fieldModel instanceof DynamicFormGroupModel) { + fieldModel.group.forEach((childModel: DynamicInputModel) => { + this.updateContentSourceField(childModel); + }); + } else { + this.updateContentSourceField(fieldModel); + } + } + ); + this.saveFieldUpdate(); + } + + updateContentSourceField(fieldModel: DynamicInputModel) { + if (hasValue(fieldModel)) { + this.contentSource[fieldModel.id] = fieldModel.value; + } + } + + saveFieldUpdate() { + this.objectUpdatesService.saveAddFieldUpdate(this.url, cloneDeep(this.contentSource)); + } + + ngOnDestroy(): void { + this.updateSub.unsubscribe(); + } } diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index 10f0986b1b..4c523b80e1 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -1,17 +1,35 @@ import { v4 as uuid } from 'uuid'; export class ContentSource { + /** + * Unique identifier + */ uuid: string; + /** + * Does this collection harvest its content from an external source ? + */ enabled = false; + /** + * OAI Provider + */ provider: string; + /** + * OAI Specific set ID + */ set: string; - format: string; + /** + * Metadata Format + */ + format = 'dc'; - harvestType: number; + /** + * Type of content being harvested + */ + harvest = 3; constructor() { this.uuid = uuid(); diff --git a/src/app/shared/trackable/abstract-trackable.component.ts b/src/app/shared/trackable/abstract-trackable.component.ts index cd1b425f10..bb1f4b31b4 100644 --- a/src/app/shared/trackable/abstract-trackable.component.ts +++ b/src/app/shared/trackable/abstract-trackable.component.ts @@ -63,7 +63,7 @@ export class AbstractTrackableComponent { * Get translated notification title * @param key */ - private getNotificationTitle(key: string) { + protected getNotificationTitle(key: string) { return this.translateService.instant(this.notificationsPrefix + key + '.title'); } @@ -71,7 +71,7 @@ export class AbstractTrackableComponent { * Get translated notification content * @param key */ - private getNotificationContent(key: string) { + protected getNotificationContent(key: string) { return this.translateService.instant(this.notificationsPrefix + key + '.content'); } diff --git a/src/config/collection-page-config.interface.ts b/src/config/collection-page-config.interface.ts new file mode 100644 index 0000000000..b0103fd176 --- /dev/null +++ b/src/config/collection-page-config.interface.ts @@ -0,0 +1,7 @@ +import { Config } from './config.interface'; + +export interface CollectionPageConfig extends Config { + edit: { + undoTimeout: number; + } +} diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index d83ec6e4d8..e2409ec18b 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -8,6 +8,7 @@ import { FormConfig } from './form-config.interfaces'; import {LangConfig} from './lang-config.interface'; import { BrowseByConfig } from './browse-by-config.interface'; import { ItemPageConfig } from './item-page-config.interface'; +import { CollectionPageConfig } from './collection-page-config.interface'; export interface GlobalConfig extends Config { ui: ServerConfig; @@ -25,4 +26,5 @@ export interface GlobalConfig extends Config { languages: LangConfig[]; browseBy: BrowseByConfig; item: ItemPageConfig; + collection: CollectionPageConfig; } From 7774c3c83c2fac48ca0e1cf1f5d64ea61f0a26b2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 12 Jul 2019 15:11:38 +0200 Subject: [PATCH 04/77] 63669: Content Source valid check + JSDocs --- .../collection-source.component.html | 4 +- .../collection-source.component.ts | 41 ++++++++++++++++++- src/app/core/shared/content-source.model.ts | 4 ++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index 2017f172c5..71f03e2a9f 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -11,7 +11,7 @@ class="fas fa-undo-alt">  {{"item.edit.metadata.reinstate-button" | translate}} - -
-

{{ 'collection.edit.tabs.source.form.head' | translate }}

+

{{ 'collection.edit.tabs.source.form.head' | translate }}

- -
+
- - + +
+
+ + + +
+

{{ 'collection.edit.tabs.source.head' | translate }}

+
+ + +
+

{{ 'collection.edit.tabs.source.form.head' | translate }}

-

{{ 'collection.edit.tabs.source.head' | translate }}

-
- - + +
+
+ + + +
-

{{ 'collection.edit.tabs.source.form.head' | translate }}

-
- -
-
- - - -
-
+
diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 70b268d9b7..932092a4a4 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,8 +1,13 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { - DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, - DynamicInputModel, DynamicOptionControlModel, DynamicRadioGroupModel, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, + DynamicFormService, + DynamicInputModel, + DynamicOptionControlModel, + DynamicRadioGroupModel, DynamicSelectModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; @@ -12,16 +17,18 @@ import { ObjectUpdatesService } from '../../../core/data/object-updates/object-u import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { FormGroup } from '@angular/forms'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { ContentSource } from '../../../core/shared/content-source.model'; +import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; -import { first, map } from 'rxjs/operators'; +import { first, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Subscription } from 'rxjs/internal/Subscription'; import { cloneDeep } from 'lodash'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; /** * Component for managing the content source of the collection @@ -64,9 +71,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * The Dynamic Input Model for the OAI Provider */ - providerModel = new DynamicInputModel({ - id: 'provider', - name: 'provider', + oaiSourceModel = new DynamicInputModel({ + id: 'oaiSource', + name: 'oaiSource', required: true, validators: { required: null @@ -79,17 +86,17 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * The Dynamic Input Model for the OAI Set */ - setModel = new DynamicInputModel({ - id: 'set', - name: 'set' + oaiSetIdModel = new DynamicInputModel({ + id: 'oaiSetId', + name: 'oaiSetId' }); /** * The Dynamic Input Model for the Metadata Format used */ - formatModel = new DynamicSelectModel({ - id: 'format', - name: 'format', + metadataConfigIdModel = new DynamicSelectModel({ + id: 'metadataConfigId', + name: 'metadataConfigId', options: [ { value: 'dc' @@ -100,24 +107,25 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem { value: 'dim' } - ] + ], + value: 'dc' }); /** * The Dynamic Input Model for the type of harvesting */ - harvestModel = new DynamicRadioGroupModel({ - id: 'harvest', - name: 'harvest', + harvestTypeModel = new DynamicRadioGroupModel({ + id: 'harvestType', + name: 'harvestType', options: [ { - value: 1 + value: ContentSourceHarvestType.Metadata }, { - value: 2 + value: ContentSourceHarvestType.MetadataAndRef }, { - value: 3 + value: ContentSourceHarvestType.MetadataAndBitstreams } ] }); @@ -125,7 +133,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * All input models in a simple array for easier iterations */ - inputModels = [this.providerModel, this.setModel, this.formatModel, this.harvestModel]; + inputModels = [this.oaiSourceModel, this.oaiSetIdModel, this.metadataConfigIdModel, this.harvestTypeModel]; /** * The dynamic form fields used for editing the content source of a collection @@ -133,22 +141,22 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ formModel: DynamicFormControlModel[] = [ new DynamicFormGroupModel({ - id: 'providerContainer', + id: 'oaiSourceContainer', group: [ - this.providerModel + this.oaiSourceModel ] }), new DynamicFormGroupModel({ - id: 'setContainer', + id: 'oaiSetContainer', group: [ - this.setModel, - this.formatModel + this.oaiSetIdModel, + this.metadataConfigIdModel ] }), new DynamicFormGroupModel({ - id: 'harvestContainer', + id: 'harvestTypeContainer', group: [ - this.harvestModel + this.harvestTypeModel ] }) ]; @@ -157,38 +165,38 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Layout used for structuring the form inputs */ formLayout: DynamicFormLayout = { - provider: { + oaiSource: { grid: { host: 'col-12 d-inline-block' } }, - set: { + oaiSetId: { grid: { host: 'col col-sm-6 d-inline-block' } }, - format: { + metadataConfigId: { grid: { host: 'col col-sm-6 d-inline-block' } }, - harvest: { + harvestType: { grid: { host: 'col-12', option: 'btn-outline-secondary' } }, - setContainer: { + oaiSetContainer: { grid: { host: 'row' } }, - providerContainer: { + oaiSourceContainer: { grid: { host: 'row' } }, - harvestContainer: { + harvestTypeContainer: { grid: { host: 'row' } @@ -205,6 +213,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ updateSub: Subscription; + harvestTypeNone = ContentSourceHarvestType.None; + public constructor(public objectUpdatesService: ObjectUpdatesService, public notificationsService: NotificationsService, protected location: Location, @@ -212,7 +222,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem protected translate: TranslateService, protected route: ActivatedRoute, protected router: Router, - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,) { + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected collectionService: CollectionDataService) { super(objectUpdatesService, notificationsService, translate); } @@ -229,16 +240,21 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.formGroup = this.formService.createFormGroup(this.formModel); this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); - /* Create a new ContentSource object - TODO: Update to be fetched from the collection */ - this.contentSource = new ContentSource(); + this.collectionRD$.pipe( + getSucceededRemoteData(), + map((col) => col.payload.uuid), + switchMap((uuid) => this.collectionService.getContentSource(uuid)), + take(1) + ).subscribe((contentSource: ContentSource) => { + this.contentSource = contentSource; + this.initializeOriginalContentSource(); + }); this.updateFieldTranslations(); this.translate.onLangChange .subscribe(() => { this.updateFieldTranslations(); }); - - this.initializeOriginalContentSource(); } /** @@ -254,15 +270,15 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem if (update) { const field = update.field as ContentSource; this.formGroup.patchValue({ - providerContainer: { - provider: field.provider + oaiSourceContainer: { + oaiSource: field.oaiSource }, - setContainer: { - set: field.set, - format: field.format + oaiSetContainer: { + oaiSetId: field.oaiSetId, + metadataConfigId: field.metadataConfigId }, - harvestContainer: { - harvest: field.harvest + harvestTypeContainer: { + harvestType: field.harvestType } }); this.contentSource = cloneDeep(field); @@ -307,7 +323,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * @param event */ onChange(event) { - this.updateContentSourceField(event.model); + this.updateContentSourceField(event.model, true); this.saveFieldUpdate(); } @@ -331,24 +347,28 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Is the current form valid to be submitted ? */ isValid(): boolean { - return !this.contentSource.enabled || this.formGroup.valid; + return (this.contentSource.harvestType === ContentSourceHarvestType.None) || this.formGroup.valid; } /** * Switch the external source on or off and fire a field update */ changeExternalSource() { - this.contentSource.enabled = !this.contentSource.enabled; - this.updateContentSource(); + if (this.contentSource.harvestType === ContentSourceHarvestType.None) { + this.contentSource.harvestType = ContentSourceHarvestType.Metadata; + } else { + this.contentSource.harvestType = ContentSourceHarvestType.None; + } + this.updateContentSource(false); } /** * Loop over all inputs and update the Content Source with their value */ - updateContentSource() { + updateContentSource(updateHarvestType: boolean) { this.inputModels.forEach( (fieldModel: DynamicInputModel) => { - this.updateContentSourceField(fieldModel) + this.updateContentSourceField(fieldModel, updateHarvestType) } ); this.saveFieldUpdate(); @@ -358,8 +378,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Update the Content Source with the value from a DynamicInputModel * @param fieldModel */ - updateContentSourceField(fieldModel: DynamicInputModel) { - if (hasValue(fieldModel.value)) { + updateContentSourceField(fieldModel: DynamicInputModel, updateHarvestType: boolean) { + if (hasValue(fieldModel.value) && !(fieldModel.id === this.harvestTypeModel.id && !updateHarvestType)) { this.contentSource[fieldModel.id] = fieldModel.value; } } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 03233e616b..c9934d6abe 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -14,6 +14,7 @@ import { DSpaceObject } from '../shared/dspace-object.model'; import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; +import { ContentSource } from '../shared/content-source.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -288,4 +289,17 @@ export class FilteredDiscoveryQueryResponse extends RestResponse { super(true, statusCode, statusText); } } + +/** + * A successful response containing exactly one MetadataSchema + */ +export class ContentSourceSuccessResponse extends RestResponse { + constructor( + public contentsource: ContentSource, + public statusCode: number, + public statusText: string, + ) { + super(true, statusCode, statusText); + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 1d22b5fefe..fe46bf88aa 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -117,6 +117,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model'; import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model'; import { BrowseDefinition } from './shared/browse-definition.model'; +import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -203,6 +204,7 @@ const PROVIDERS = [ TaskResponseParsingService, ClaimedTaskDataService, PoolTaskDataService, + ContentSourceResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 993954a360..1a59498a72 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { filter, map, take } from 'rxjs/operators'; +import { filter, map, take, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -16,9 +16,12 @@ import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { Observable } from 'rxjs/internal/Observable'; -import { FindAllOptions } from './request.models'; +import { ContentSourceRequest, FindAllOptions, RestRequest } from './request.models'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; +import { ContentSource } from '../shared/content-source.model'; +import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../shared/operators'; +import { ContentSourceSuccessResponse } from '../cache/response.models'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -57,4 +60,21 @@ export class CollectionDataService extends ComColDataService { map((collections: RemoteData>) => collections.payload.totalElements > 0) ); } + + getHarvesterEndpoint(collectionId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + map((href: string) => `${href}/${collectionId}/harvester`) + ); + } + + getContentSource(collectionId: string): Observable { + return this.getHarvesterEndpoint(collectionId).pipe( + map((href: string) => new ContentSourceRequest(this.requestService.generateRequestId(), href)), + configureRequest(this.requestService), + map((request: RestRequest) => request.href), + getRequestFromRequestHref(this.requestService), + filterSuccessfulResponses(), + map((response: ContentSourceSuccessResponse) => response.contentsource) + ); + } } diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts new file mode 100644 index 0000000000..34e9360dd7 --- /dev/null +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { ContentSource } from '../shared/content-source.model'; + +@Injectable() +export class ContentSourceResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + + const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload); + return new ContentSourceSuccessResponse(deserialized, data.statusCode, data.statusText); + } + +} 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 22d5fd3e77..2b96cb378a 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -93,14 +93,16 @@ export class ObjectUpdatesService { const objectUpdates = this.getObjectEntry(url); return objectUpdates.pipe(map((objectEntry) => { const fieldUpdates: FieldUpdates = {}; - Object.keys(objectEntry.fieldStates).forEach((uuid) => { - let fieldUpdate = objectEntry.fieldUpdates[uuid]; - if (isEmpty(fieldUpdate)) { - const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); - fieldUpdate = { field: identifiable, changeType: undefined }; - } - fieldUpdates[uuid] = fieldUpdate; - }); + if (hasValue(objectEntry)) { + Object.keys(objectEntry.fieldStates).forEach((uuid) => { + let fieldUpdate = objectEntry.fieldUpdates[uuid]; + if (isEmpty(fieldUpdate)) { + const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); + fieldUpdate = {field: identifiable, changeType: undefined}; + } + fieldUpdates[uuid] = fieldUpdate; + }); + } return fieldUpdates; })) } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index a2b3423960..eab7af5751 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; +import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -358,6 +359,16 @@ export class CreateRequest extends PostRequest { } } +export class ContentSourceRequest extends GetRequest { + constructor(uuid: string, href: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return ContentSourceResponseParsingService; + } +} + /** * Request to delete an object based on its identifier */ diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index c6d6c03fab..68db549adb 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -1,41 +1,51 @@ -import { v4 as uuid } from 'uuid'; +import { autoserialize, autoserializeAs } from 'cerialize'; + +export enum ContentSourceHarvestType { + None = 'NONE', + Metadata = 'METADATA_ONLY', + MetadataAndRef = 'METADATA_AND_REF', + MetadataAndBitstreams = 'METADATA_AND_BITSTREAMS' +} /** * A model class that holds information about the Content Source of a Collection */ export class ContentSource { /** - * Unique identifier + * Unique identifier, this is necessary to store the ContentSource in FieldUpdates + * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink */ + @autoserializeAs('self') uuid: string; /** - * Does this collection harvest its content from an external source ? + * OAI Provider / Source */ - enabled = false; - - /** - * OAI Provider - */ - provider: string; + @autoserializeAs('oai_source') + oaiSource: string; /** * OAI Specific set ID */ - set: string; + @autoserializeAs('oai_set_id') + oaiSetId: string; /** - * Metadata Format + * The ID of the metadata format used */ - format = 'dc'; + @autoserializeAs('metadata_config_id') + metadataConfigId = 'dc'; /** * Type of content being harvested + * Defaults to 'NONE', meaning the collection doesn't harvest its content from an external source */ - harvest = 3; + @autoserializeAs('harvest_type') + harvestType = ContentSourceHarvestType.None; - constructor() { - // TODO: Remove this once the Content Source is fetched from the REST API and a custom generated UUID is not necessary anymore - this.uuid = uuid(); - } + /** + * The REST link to itself + */ + @autoserialize + self: string; } From 1b9bc2f7a3fc6f03e18a1ffe15fc8be297f88ba5 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 20 Aug 2019 17:28:28 +0200 Subject: [PATCH 11/77] 64503: Content Source PUT request on form submit --- resources/i18n/en.json | 2 + .../collection-source.component.ts | 13 +++- src/app/core/data/collection-data.service.ts | 67 +++++++++++++++++-- src/app/core/data/request.models.ts | 10 +++ src/app/core/shared/content-source.model.ts | 6 +- 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 41a4bc3f29..0fb6fd4229 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -131,6 +131,8 @@ "collection.page.browse.recent.head": "Recent Submissions", "collection.page.license": "License", "collection.page.news": "News", + "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", + "collection.source.update.notifications.error.title": "Server Error", "community.create.head": "Create a Community", "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.delete.cancel": "Cancel", diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 932092a4a4..92dba10ce8 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -331,9 +331,16 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Submit the edited Content Source to the REST API, re-initialize the field update and display a notification */ onSubmit() { - // TODO: Fetch field update and send to REST API - this.initializeOriginalContentSource(); - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + this.collectionRD$.pipe( + getSucceededRemoteData(), + map((col) => col.payload.uuid), + switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)), + take(1) + ).subscribe((contentSource: ContentSource) => { + this.contentSource = contentSource; + this.initializeOriginalContentSource(); + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + }); } /** diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 1a59498a72..a858469466 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -12,21 +12,33 @@ import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { Observable } from 'rxjs/internal/Observable'; -import { ContentSourceRequest, FindAllOptions, RestRequest } from './request.models'; +import { ContentSourceRequest, FindAllOptions, RestRequest, UpdateContentSourceRequest } from './request.models'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; import { ContentSource } from '../shared/content-source.model'; -import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../shared/operators'; -import { ContentSourceSuccessResponse } from '../cache/response.models'; +import { + configureRequest, + filterSuccessfulResponses, + getRequestFromRequestHref, + getResponseFromEntry +} from '../shared/operators'; +import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { TranslateService } from '@ngx-translate/core'; @Injectable() export class CollectionDataService extends ComColDataService { protected linkPath = 'collections'; protected forceBypassCache = false; + protected errorTitle = 'collection.source.update.notifications.error.title'; + protected contentSourceError = 'collection.source.update.notifications.error.content'; constructor( protected requestService: RequestService, @@ -38,7 +50,8 @@ export class CollectionDataService extends ComColDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOChangeAnalyzer + protected comparator: DSOChangeAnalyzer, + protected translate: TranslateService ) { super(); } @@ -77,4 +90,48 @@ export class CollectionDataService extends ComColDataService { map((response: ContentSourceSuccessResponse) => response.contentsource) ); } + + updateContentSource(collectionId: string, contentSource: ContentSource): Observable { + const requestId = this.requestService.generateRequestId(); + const serializedContentSource = new DSpaceRESTv2Serializer(ContentSource).serialize(contentSource); + const request$ = this.getHarvesterEndpoint(collectionId).pipe( + take(1), + map((href: string) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + return new UpdateContentSourceRequest(requestId, href, JSON.stringify(serializedContentSource), options); + }) + ); + + // Execute the post/put request + request$.pipe( + configureRequest(this.requestService) + ).subscribe(); + + // Return updated ContentSource + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + if (!response.isSuccessful) { + if (hasValue((response as any).errorMessage)) { + if (response.statusCode === 422) { + this.notificationsService.error(this.translate.instant(this.errorTitle), this.translate.instant(this.contentSourceError), new NotificationOptions(-1)); + } else { + this.notificationsService.error(this.translate.instant(this.errorTitle), (response as any).errorMessage, new NotificationOptions(-1)); + } + } + } else { + return response; + } + }), + isNotEmptyOperator(), + map((response: ContentSourceSuccessResponse) => { + if (isNotEmpty(response.contentsource)) { + return response.contentsource; + } + }) + ); + } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index eab7af5751..f25b084af7 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -369,6 +369,16 @@ export class ContentSourceRequest extends GetRequest { } } +export class UpdateContentSourceRequest extends PutRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor { + return ContentSourceResponseParsingService; + } +} + /** * Request to delete an object based on its identifier */ diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index 68db549adb..76df1f10ce 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, autoserializeAs, deserializeAs, deserialize } from 'cerialize'; export enum ContentSourceHarvestType { None = 'NONE', @@ -15,7 +15,7 @@ export class ContentSource { * Unique identifier, this is necessary to store the ContentSource in FieldUpdates * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink */ - @autoserializeAs('self') + @deserializeAs('self') uuid: string; /** @@ -46,6 +46,6 @@ export class ContentSource { /** * The REST link to itself */ - @autoserialize + @deserialize self: string; } From 8103321245b7c0345d75ffa41b351698af5530fb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 20 Aug 2019 17:54:58 +0200 Subject: [PATCH 12/77] 64503: MetadataConfig in ContentSource + JSDocs --- .../collection-source.component.ts | 7 ++++++- src/app/core/data/collection-data.service.ts | 13 +++++++++++++ ...content-source-response-parsing.service.ts | 12 ++++++++++++ src/app/core/shared/content-source.model.ts | 9 +++++++++ src/app/core/shared/metadata-config.model.ts | 19 +++++++++++++++++++ 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/app/core/shared/metadata-config.model.ts diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 92dba10ce8..78c7badf51 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -213,6 +213,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ updateSub: Subscription; + /** + * The content harvesting type used when harvesting is disabled + */ harvestTypeNone = ContentSourceHarvestType.None; public constructor(public objectUpdatesService: ObjectUpdatesService, @@ -371,6 +374,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * Loop over all inputs and update the Content Source with their value + * @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update */ updateContentSource(updateHarvestType: boolean) { this.inputModels.forEach( @@ -383,7 +387,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * Update the Content Source with the value from a DynamicInputModel - * @param fieldModel + * @param fieldModel The fieldModel to fetch the value from and update the contentSource with + * @param updateHarvestType When set to false, the harvestType of the contentSource will be ignored in the update */ updateContentSourceField(fieldModel: DynamicInputModel, updateHarvestType: boolean) { if (hasValue(fieldModel.value) && !(fieldModel.id === this.harvestTypeModel.id && !updateHarvestType)) { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index a858469466..fedb666052 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -74,12 +74,20 @@ export class CollectionDataService extends ComColDataService { ); } + /** + * Get the endpoint for the collection's content harvester + * @param collectionId + */ getHarvesterEndpoint(collectionId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((href: string) => `${href}/${collectionId}/harvester`) ); } + /** + * Get the collection's content harvester + * @param collectionId + */ getContentSource(collectionId: string): Observable { return this.getHarvesterEndpoint(collectionId).pipe( map((href: string) => new ContentSourceRequest(this.requestService.generateRequestId(), href)), @@ -91,6 +99,11 @@ export class CollectionDataService extends ComColDataService { ); } + /** + * Update the settings of the collection's content harvester + * @param collectionId + * @param contentSource + */ updateContentSource(collectionId: string, contentSource: ContentSource): Observable { const requestId = this.requestService.generateRequestId(); const serializedContentSource = new DSpaceRESTv2Serializer(ContentSource).serialize(contentSource); diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index 34e9360dd7..649a8f9b96 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -5,14 +5,26 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { ContentSource } from '../shared/content-source.model'; +import { MetadataConfig } from '../shared/metadata-config.model'; @Injectable() +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a ContentSource object + * wrapped in a ContentSourceSuccessResponse + */ export class ContentSourceResponseParsingService implements ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload; const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload); + + let metadataConfigs = []; + if (payload._embedded && payload._embedded.metadata_configs && payload._embedded.metadata_configs.configs) { + metadataConfigs = new DSpaceRESTv2Serializer(MetadataConfig).serializeArray(payload._embedded.metadata_configs.configs); + } + deserialized.metadataConfigs = metadataConfigs; + return new ContentSourceSuccessResponse(deserialized, data.statusCode, data.statusText); } diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index 76df1f10ce..a2df90027f 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -1,5 +1,9 @@ import { autoserialize, autoserializeAs, deserializeAs, deserialize } from 'cerialize'; +import { MetadataConfig } from './metadata-config.model'; +/** + * The type of content harvesting used + */ export enum ContentSourceHarvestType { None = 'NONE', Metadata = 'METADATA_ONLY', @@ -43,6 +47,11 @@ export class ContentSource { @autoserializeAs('harvest_type') harvestType = ContentSourceHarvestType.None; + /** + * The available metadata configurations + */ + metadataConfigs: MetadataConfig[]; + /** * The REST link to itself */ diff --git a/src/app/core/shared/metadata-config.model.ts b/src/app/core/shared/metadata-config.model.ts new file mode 100644 index 0000000000..861d04586e --- /dev/null +++ b/src/app/core/shared/metadata-config.model.ts @@ -0,0 +1,19 @@ +/** + * A model class that holds information about a certain metadata configuration + */ +export class MetadataConfig { + /** + * A unique indentifier + */ + id: string; + + /** + * The label used for display + */ + label: string; + + /** + * The namespace of the metadata + */ + nameSpace: string; +} From bf8e2078bcaeb5592debcbf9de6c1428429af597 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 10:40:58 +0200 Subject: [PATCH 13/77] 64503: Metadata Format dropdown filled in by metadataConfig from REST response --- .../collection-source.component.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 78c7badf51..5a1f40a1e2 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -16,7 +16,7 @@ import { TranslateService } from '@ngx-translate/core'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { FormGroup } from '@angular/forms'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; @@ -29,6 +29,7 @@ import { cloneDeep } from 'lodash'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { MetadataConfig } from '../../../core/shared/metadata-config.model'; /** * Component for managing the content source of the collection @@ -96,19 +97,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ metadataConfigIdModel = new DynamicSelectModel({ id: 'metadataConfigId', - name: 'metadataConfigId', - options: [ - { - value: 'dc' - }, - { - value: 'qdc' - }, - { - value: 'dim' - } - ], - value: 'dc' + name: 'metadataConfigId' }); /** @@ -249,8 +238,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem switchMap((uuid) => this.collectionService.getContentSource(uuid)), take(1) ).subscribe((contentSource: ContentSource) => { - this.contentSource = contentSource; - this.initializeOriginalContentSource(); + this.initializeOriginalContentSource(contentSource); }); this.updateFieldTranslations(); @@ -263,7 +251,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * Initialize the Field Update and subscribe on it to fire updates to the form whenever it changes */ - initializeOriginalContentSource() { + initializeOriginalContentSource(contentSource: ContentSource) { + this.contentSource = contentSource; + this.initializeMetadataConfigs(); const initialContentSource = cloneDeep(this.contentSource); this.objectUpdatesService.initialize(this.url, [initialContentSource], new Date()); this.update$ = this.objectUpdatesService.getFieldUpdates(this.url, [initialContentSource]).pipe( @@ -289,6 +279,17 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem }); } + /** + * Fill the metadataConfigIdModel's options using the contentSource's metadataConfigs property + */ + initializeMetadataConfigs() { + this.metadataConfigIdModel.options = this.contentSource.metadataConfigs + .map((metadataConfig: MetadataConfig) => Object.assign({ value: metadataConfig.id, label: metadataConfig.label })); + if (this.metadataConfigIdModel.options.length > 0) { + this.metadataConfigIdModel.value = this.metadataConfigIdModel.options[0].value; + } + } + /** * Used the update translations of errors and labels on init and on language change */ @@ -315,7 +316,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem if (fieldModel instanceof DynamicOptionControlModel) { if (isNotEmpty(fieldModel.options)) { fieldModel.options.forEach((option) => { - option.label = this.translate.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value); + if (hasNoValue(option.label)) { + option.label = this.translate.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value); + } }); } } @@ -340,8 +343,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)), take(1) ).subscribe((contentSource: ContentSource) => { - this.contentSource = contentSource; - this.initializeOriginalContentSource(); + this.initializeOriginalContentSource(contentSource); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); }); } From a3b6917abac248969f385b0ddf15d9db44ce7133 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 11:24:52 +0200 Subject: [PATCH 14/77] 64503: Remembering previously selected harvest type + small dropdown fix --- .../collection-source.component.ts | 16 ++++++++++++++-- src/app/core/shared/content-source.model.ts | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 5a1f40a1e2..d4d551d61d 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -207,6 +207,13 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ harvestTypeNone = ContentSourceHarvestType.None; + /** + * The previously selected harvesting type + * Used for switching between ContentSourceHarvestType.None and the previously selected value when enabling / disabling harvesting + * Defaults to ContentSourceHarvestType.Metadata + */ + previouslySelectedHarvestType = ContentSourceHarvestType.Metadata; + public constructor(public objectUpdatesService: ObjectUpdatesService, public notificationsService: NotificationsService, protected location: Location, @@ -286,7 +293,11 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.metadataConfigIdModel.options = this.contentSource.metadataConfigs .map((metadataConfig: MetadataConfig) => Object.assign({ value: metadataConfig.id, label: metadataConfig.label })); if (this.metadataConfigIdModel.options.length > 0) { - this.metadataConfigIdModel.value = this.metadataConfigIdModel.options[0].value; + this.formGroup.patchValue({ + oaiSetContainer: { + metadataConfigId: this.metadataConfigIdModel.options[0].value + } + }); } } @@ -367,8 +378,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ changeExternalSource() { if (this.contentSource.harvestType === ContentSourceHarvestType.None) { - this.contentSource.harvestType = ContentSourceHarvestType.Metadata; + this.contentSource.harvestType = this.previouslySelectedHarvestType; } else { + this.previouslySelectedHarvestType = this.contentSource.harvestType; this.contentSource.harvestType = ContentSourceHarvestType.None; } this.updateContentSource(false); diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index a2df90027f..cd53c2d81e 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -38,7 +38,7 @@ export class ContentSource { * The ID of the metadata format used */ @autoserializeAs('metadata_config_id') - metadataConfigId = 'dc'; + metadataConfigId: string; /** * Type of content being harvested From 9fab40d7e9e2c0afb3702602c8692b6e3ecd9abf Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 14:14:01 +0200 Subject: [PATCH 15/77] 64503: CollectionSourceComponent and CollectionDataService tests --- .../collection-source.component.spec.ts | 51 ++++++-- .../core/data/collection-data.service.spec.ts | 109 ++++++++++++++++++ 2 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 src/app/core/data/collection-data.service.spec.ts diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index 89d7445b65..8e93cd7bdd 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CollectionSourceComponent } from './collection-source.component'; -import { ContentSource } from '../../../core/shared/content-source.model'; +import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; @@ -17,6 +17,9 @@ import { FormControl, FormGroup } from '@angular/forms'; import { RouterStub } from '../../../shared/testing/router-stub'; import { GLOBAL_CONFIG } from '../../../../config'; import { By } from '@angular/platform-browser'; +import { Collection } from '../../../core/shared/collection.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -31,6 +34,8 @@ let notificationsService: NotificationsService; let location: Location; let formService: DynamicFormService; let router: Router; +let collection: Collection; +let collectionService: CollectionDataService; describe('CollectionSourceComponent', () => { let comp: CollectionSourceComponent; @@ -39,7 +44,24 @@ describe('CollectionSourceComponent', () => { beforeEach(async(() => { date = new Date(); contentSource = Object.assign(new ContentSource(), { - uuid: uuid + uuid: uuid, + metadataConfigs: [ + { + id: 'dc', + label: 'Simple Dublin Core', + nameSpace: 'http://www.openarchives.org/OAI/2.0/oai_dc/' + }, + { + id: 'qdc', + label: 'Qualified Dublin Core', + nameSpace: 'http://purl.org/dc/terms/' + }, + { + id: 'dim', + label: 'DSpace Intermediate Metadata', + nameSpace: 'http://www.dspace.org/xmlns/dspace/dim' + } + ] }); fieldUpdate = { field: contentSource, @@ -84,6 +106,13 @@ describe('CollectionSourceComponent', () => { router = Object.assign(new RouterStub(), { url: 'http://test-url.com/test-url' }); + collection = Object.assign(new Collection(), { + uuid: 'fake-collection-id' + }); + collectionService = jasmine.createSpyObj('collectionService', { + getContentSource: observableOf(contentSource), + updateContentSource: observableOf(contentSource) + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], @@ -93,9 +122,10 @@ describe('CollectionSourceComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: Location, useValue: location }, { provide: DynamicFormService, useValue: formService }, - { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: new RemoteData(false, false, true, null, collection) }) } } }, { provide: Router, useValue: router }, { provide: GLOBAL_CONFIG, useValue: { collection: { edit: { undoTimeout: 10 } } } as any }, + { provide: CollectionDataService, useValue: collectionService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -104,7 +134,6 @@ describe('CollectionSourceComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CollectionSourceComponent); comp = fixture.componentInstance; - comp.contentSource = contentSource; fixture.detectChanges(); }); @@ -116,7 +145,7 @@ describe('CollectionSourceComponent', () => { }); it('ContentSource should be disabled', () => { - expect(comp.contentSource.enabled).toBe(false); + expect(comp.contentSource.harvestType).toEqual(ContentSourceHarvestType.None); }); it('the input-form should be hidden', () => { @@ -136,7 +165,7 @@ describe('CollectionSourceComponent', () => { }); it('should enable ContentSource', () => { - expect(comp.contentSource.enabled).toBe(true); + expect(comp.contentSource.harvestType).not.toEqual(ContentSourceHarvestType.None); }); it('should send a field update', () => { @@ -151,19 +180,19 @@ describe('CollectionSourceComponent', () => { describe('isValid', () => { it('should return true when ContentSource is disabled but the form invalid', () => { spyOnProperty(comp.formGroup, 'valid').and.returnValue(false); - comp.contentSource.enabled = false; + comp.contentSource.harvestType = ContentSourceHarvestType.None; expect(comp.isValid()).toBe(true); }); it('should return false when ContentSource is enabled but the form is invalid', () => { spyOnProperty(comp.formGroup, 'valid').and.returnValue(false); - comp.contentSource.enabled = true; + comp.contentSource.harvestType = ContentSourceHarvestType.Metadata; expect(comp.isValid()).toBe(false); }); it('should return true when ContentSource is enabled and the form is valid', () => { spyOnProperty(comp.formGroup, 'valid').and.returnValue(true); - comp.contentSource.enabled = true; + comp.contentSource.harvestType = ContentSourceHarvestType.Metadata; expect(comp.isValid()).toBe(true); }); }); @@ -181,6 +210,8 @@ describe('CollectionSourceComponent', () => { expect(notificationsService.success).toHaveBeenCalled(); }); - // TODO: Write test for sending data to REST API on submit + it('should call updateContentSource on the collectionService', () => { + expect(collectionService.updateContentSource).toHaveBeenCalled(); + }); }); }); diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts new file mode 100644 index 0000000000..f0d3f63910 --- /dev/null +++ b/src/app/core/data/collection-data.service.spec.ts @@ -0,0 +1,109 @@ +import { CollectionDataService } from './collection-data.service'; +import { RequestService } from './request.service'; +import { TranslateService } from '@ngx-translate/core'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; +import { getMockTranslateService } from '../../shared/mocks/mock-translate.service'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { ContentSourceRequest, RequestError, UpdateContentSourceRequest } from './request.models'; +import { ContentSource } from '../shared/content-source.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RequestEntry } from './request.reducer'; +import { ErrorResponse, RestResponse } from '../cache/response.models'; + +const url = 'fake-url'; +const collectionId = 'fake-collection-id'; + +describe('CollectionDataService', () => { + let service: CollectionDataService; + + let requestService: RequestService; + let translate: TranslateService; + let notificationsService: any; + let halService: any; + + describe('when the requests are successful', () => { + beforeEach(() => { + createService(); + }); + + describe('when calling getContentSource', () => { + let contentSource$; + + beforeEach(() => { + contentSource$ = service.getContentSource(collectionId); + }); + + it('should configure a new ContentSourceRequest', fakeAsync(() => { + contentSource$.subscribe(); + tick(); + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(ContentSourceRequest), undefined); + })); + }); + + describe('when calling updateContentSource', () => { + let returnedContentSource$; + let contentSource; + + beforeEach(() => { + contentSource = new ContentSource(); + returnedContentSource$ = service.updateContentSource(collectionId, contentSource); + }); + + it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { + returnedContentSource$.subscribe(); + tick(); + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest), undefined); + })); + }); + }); + + describe('when the requests are unsuccessful', () => { + beforeEach(() => { + createService(observableOf(Object.assign(new RequestEntry(), { + response: new ErrorResponse(Object.assign({ + statusCode: 422, + statusText: 'Unprocessable Entity', + message: 'Error message' + })) + }))); + }); + + describe('when calling updateContentSource', () => { + let returnedContentSource$; + let contentSource; + + beforeEach(() => { + contentSource = new ContentSource(); + returnedContentSource$ = service.updateContentSource(collectionId, contentSource); + }); + + it('should configure a new UpdateContentSourceRequest', fakeAsync(() => { + returnedContentSource$.subscribe(); + tick(); + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(UpdateContentSourceRequest), undefined); + })); + + it('should display an error notification', fakeAsync(() => { + returnedContentSource$.subscribe(); + tick(); + expect(notificationsService.error).toHaveBeenCalled(); + })); + }); + }); + + /** + * Create a CollectionDataService used for testing + * @param requestEntry$ Supply a requestEntry to be returned by the REST API (optional) + */ + function createService(requestEntry$?) { + requestService = getMockRequestService(requestEntry$); + halService = new HALEndpointServiceStub(url); + notificationsService = new NotificationsServiceStub(); + translate = getMockTranslateService(); + + service = new CollectionDataService(requestService, null, null, null, null, null, halService, notificationsService, null, null, translate); + } + +}); From 86555b936db49a03d2b93654acc1abe6818a3c44 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 14:22:54 +0200 Subject: [PATCH 16/77] 64503: Small bugfix on login --- .../collection-source.component.html | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index 42f58b4692..d02637ed80 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -1,57 +1,55 @@ - -
-
- - - -
-

{{ 'collection.edit.tabs.source.head' | translate }}

-
- - -
-

{{ 'collection.edit.tabs.source.form.head' | translate }}

+
+
+ + +
- -
-
- - - -
+

{{ 'collection.edit.tabs.source.head' | translate }}

+
+ +
- +

{{ 'collection.edit.tabs.source.form.head' | translate }}

+
+ +
+
+ + + +
+
From a3116a3c5ad962e28011d1233e9673e2871a9475 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 14:37:14 +0200 Subject: [PATCH 17/77] 64503: AoT build error fixes --- .../collection-source/collection-source.component.spec.ts | 2 +- .../edit-collection-page/edit-collection-page.component.ts | 2 +- .../edit-community-page/edit-community-page.component.ts | 2 +- .../edit-comcol-page/edit-comcol-page.component.ts | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index 8e93cd7bdd..a44311a1c7 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -33,7 +33,7 @@ let objectUpdatesService: ObjectUpdatesService; let notificationsService: NotificationsService; let location: Location; let formService: DynamicFormService; -let router: Router; +let router: any; let collection: Collection; let collectionService: CollectionDataService; diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts index 21671fe112..dc6df135b4 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts @@ -12,7 +12,7 @@ import { getCollectionPageRoute } from '../collection-page-routing.module'; templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCollectionPageComponent extends EditComColPageComponent { - protected type = 'collection'; + public type = 'collection'; public constructor( protected router: Router, diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index a8d4d32b7d..09fc3510a3 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -12,7 +12,7 @@ import { getCommunityPageRoute } from '../community-page-routing.module'; templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCommunityPageComponent extends EditComColPageComponent { - protected type = 'community'; + public type = 'community'; public constructor( protected router: Router, diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts index 9face94abd..a750e76c87 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -2,10 +2,8 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../empty.util'; +import { isNotEmpty } from '../../empty.util'; import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; -import { DataService } from '../../../core/data/data.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; /** @@ -19,7 +17,7 @@ export class EditComColPageComponent implements On /** * The type of DSpaceObject (used to create i18n messages) */ - protected type: string; + public type: string; /** * The current page outlet string From ff2083941a3cc4598a73098bd5e02042aa15e13b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Aug 2019 14:42:35 +0200 Subject: [PATCH 18/77] 64503: Removal of unnecessary messages --- resources/i18n/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 0fb6fd4229..88acc61d3c 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -109,9 +109,6 @@ "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_BITSTREAMS": "Harvest metadata and bitstreams (requires ORE support)", "collection.edit.tabs.source.form.options.harvestType.METADATA_AND_REF": "Harvest metadata and references to bitstreams (requires ORE support)", "collection.edit.tabs.source.form.options.harvestType.METADATA_ONLY": "Harvest metadata only", - "collection.edit.tabs.source.form.options.metadataConfigId.dc": "Simple Dublin Core", - "collection.edit.tabs.source.form.options.metadataConfigId.dim": "DSpace Intermediate Metadata", - "collection.edit.tabs.source.form.options.metadataConfigId.qdc": "Qualified Dublin Core", "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "collection.edit.tabs.source.notifications.discarded.title": "Changed discarded", From 7432297dce35c20f8ea058e59cdba503a30bab80 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 3 Sep 2019 11:19:23 +0200 Subject: [PATCH 19/77] 64503: Fetch harvester endpoint using halService + small unsubscribe fix --- .../collection-source/collection-source.component.ts | 4 +++- src/app/core/data/collection-data.service.ts | 4 ++-- src/app/core/shared/hal-endpoint.service.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index d4d551d61d..3a87f11910 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -421,6 +421,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Make sure open subscriptions are closed */ ngOnDestroy(): void { - this.updateSub.unsubscribe(); + if (this.updateSub) { + this.updateSub.unsubscribe(); + } } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index fedb666052..435bee21b0 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { filter, map, take, tap } from 'rxjs/operators'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -80,7 +80,7 @@ export class CollectionDataService extends ComColDataService { */ getHarvesterEndpoint(collectionId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - map((href: string) => `${href}/${collectionId}/harvester`) + switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`)) ); } diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index a93d54db64..117cc074ca 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -43,8 +43,8 @@ export class HALEndpointService { ); } - public getEndpoint(linkPath: string): Observable { - return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); + public getEndpoint(linkPath: string, startHref?: string): Observable { + return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/')); } /** From e59b0929eded9fe343b1e31b3cc3b8d81cb0101b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 3 Sep 2019 16:11:31 +0200 Subject: [PATCH 20/77] 64503: Default metadataConfigID --- .../collection-source/collection-source.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 3a87f11910..a050443353 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -269,19 +269,21 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.updateSub = this.update$.subscribe((update: FieldUpdate) => { if (update) { const field = update.field as ContentSource; + const defaultConfigId = this.contentSource.metadataConfigs[0].id; this.formGroup.patchValue({ oaiSourceContainer: { oaiSource: field.oaiSource }, oaiSetContainer: { oaiSetId: field.oaiSetId, - metadataConfigId: field.metadataConfigId + metadataConfigId: field.metadataConfigId || defaultConfigId }, harvestTypeContainer: { harvestType: field.harvestType } }); this.contentSource = cloneDeep(field); + this.contentSource.metadataConfigId = defaultConfigId; } }); } From 404729faa3aea88c7e5eed526d7d8cdbaf1ef58a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 31 Oct 2019 12:41:03 +0100 Subject: [PATCH 21/77] 64503: Feedback 2019-10-24 ; loading, message fix, notification clearing --- resources/i18n/en.json5 | 3 +- .../collection-source.component.html | 5 +-- .../collection-source.component.ts | 34 ++++++++++++++++--- src/app/core/data/collection-data.service.ts | 14 ++++---- ...content-source-response-parsing.service.ts | 4 +-- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index a9078373ae..f83ed568da 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -159,7 +159,7 @@ "collection.edit.tabs.roles.head": "Assign Roles", "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.source.external": "This collection harvests its content from an external source", - "collection.edit.tabs.source.form.errors.provider.required": "You must provide a set id of the target collection.", + "collection.edit.tabs.source.form.errors.oaiSource.required": "You must provide a set id of the target collection.", "collection.edit.tabs.source.form.harvestType": "Content being harvested", "collection.edit.tabs.source.form.head": "Configure an external source", "collection.edit.tabs.source.form.metadataConfigId": "Metadata Format", @@ -478,6 +478,7 @@ "loading.browse-by-page": "Loading page...", "loading.collection": "Loading collection...", "loading.collections": "Loading collections...", + "loading.content-source": "Loading content source...", "loading.community": "Loading community...", "loading.default": "Loading...", "loading.item": "Loading item...", diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index d02637ed80..dfcd6e3166 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -18,11 +18,12 @@

{{ 'collection.edit.tabs.source.head' | translate }}

-
+
-

{{ 'collection.edit.tabs.source.form.head' | translate }}

+ +

{{ 'collection.edit.tabs.source.form.head' | translate }}

{ if (update) { const field = update.field as ContentSource; - const defaultConfigId = this.contentSource.metadataConfigs[0].id; + let defaultConfigId; + if (hasValue(this.contentSource) && isNotEmpty(this.contentSource.metadataConfigs)) { + defaultConfigId = this.contentSource.metadataConfigs[0].id; + } this.formGroup.patchValue({ oaiSourceContainer: { oaiSource: field.oaiSource @@ -355,9 +366,14 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)), take(1) - ).subscribe((contentSource: ContentSource) => { - this.initializeOriginalContentSource(contentSource); - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + ).subscribe((result: ContentSource | INotification) => { + if (hasValue((result as any).harvestType)) { + this.clearNotifications(); + this.initializeOriginalContentSource(result as ContentSource); + this.displayedNotifications.push(this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'))); + } else { + this.displayedNotifications.push(result as INotification); + } }); } @@ -419,6 +435,16 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.objectUpdatesService.saveAddFieldUpdate(this.url, cloneDeep(this.contentSource)); } + /** + * Clear possible active notifications + */ + clearNotifications() { + this.displayedNotifications.forEach((notification: INotification) => { + this.notificationsService.remove(notification); + }); + this.displayedNotifications = []; + } + /** * Make sure open subscriptions are closed */ diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 94860e5df7..e53b684539 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -38,6 +38,7 @@ import { ResponseParsingService } from './parsing.service'; import { GenericConstructor } from '../shared/generic-constructor'; import { DSpaceObject } from '../shared/dspace-object.model'; import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; +import { INotification } from '../../shared/notifications/models/notification.model'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -140,7 +141,7 @@ export class CollectionDataService extends ComColDataService { * @param collectionId * @param contentSource */ - updateContentSource(collectionId: string, contentSource: ContentSource): Observable { + updateContentSource(collectionId: string, contentSource: ContentSource): Observable { const requestId = this.requestService.generateRequestId(); const serializedContentSource = new DSpaceRESTv2Serializer(ContentSource).serialize(contentSource); const request$ = this.getHarvesterEndpoint(collectionId).pipe( @@ -166,9 +167,9 @@ export class CollectionDataService extends ComColDataService { if (!response.isSuccessful) { if (hasValue((response as any).errorMessage)) { if (response.statusCode === 422) { - this.notificationsService.error(this.translate.instant(this.errorTitle), this.translate.instant(this.contentSourceError), new NotificationOptions(-1)); + return this.notificationsService.error(this.translate.instant(this.errorTitle), this.translate.instant(this.contentSourceError), new NotificationOptions(-1)); } else { - this.notificationsService.error(this.translate.instant(this.errorTitle), (response as any).errorMessage, new NotificationOptions(-1)); + return this.notificationsService.error(this.translate.instant(this.errorTitle), (response as any).errorMessage, new NotificationOptions(-1)); } } } else { @@ -176,10 +177,11 @@ export class CollectionDataService extends ComColDataService { } }), isNotEmptyOperator(), - map((response: ContentSourceSuccessResponse) => { - if (isNotEmpty(response.contentsource)) { - return response.contentsource; + map((response: ContentSourceSuccessResponse | INotification) => { + if (isNotEmpty((response as any).contentsource)) { + return (response as ContentSourceSuccessResponse).contentsource; } + return response as INotification; }) ); } diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index 649a8f9b96..4e0490148b 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -20,8 +20,8 @@ export class ContentSourceResponseParsingService implements ResponseParsingServi const deserialized = new DSpaceRESTv2Serializer(ContentSource).deserialize(payload); let metadataConfigs = []; - if (payload._embedded && payload._embedded.metadata_configs && payload._embedded.metadata_configs.configs) { - metadataConfigs = new DSpaceRESTv2Serializer(MetadataConfig).serializeArray(payload._embedded.metadata_configs.configs); + if (payload._embedded && payload._embedded.harvestermetadata && payload._embedded.harvestermetadata.configs) { + metadataConfigs = new DSpaceRESTv2Serializer(MetadataConfig).serializeArray(payload._embedded.harvestermetadata.configs); } deserialized.metadataConfigs = metadataConfigs; From 2d3d7ae71ee9d1bc1d6be973ab9fa556ddef8a38 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 12 Nov 2019 11:59:44 +0100 Subject: [PATCH 22/77] 64503: Prevent submit when harvest type is NONE and not changed --- .../collection-source.component.html | 4 +-- .../collection-source.component.ts | 35 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html index dfcd6e3166..4192922c7e 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -11,7 +11,7 @@ class="fas fa-undo-alt">  {{"item.edit.metadata.reinstate-button" | translate}} - - - - - - +
    + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index b78385dc62..f34287fd48 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -6,7 +6,8 @@ import { EventEmitter, Input, NgZone, - OnChanges, OnDestroy, + OnChanges, + OnDestroy, OnInit, Output, QueryList, @@ -49,7 +50,6 @@ import { DynamicNGBootstrapTimePickerComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; -import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -71,9 +71,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; -import { map, switchMap, take, tap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; -import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; +import { map, startWith, switchMap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -82,25 +81,18 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { - getAllSucceededRemoteData, - getRemoteDataPayload, - getSucceededRemoteData, - obsLog -} from '../../../../core/shared/operators'; +import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../app.reducer'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; -import { MetadataValue } from '../../../../core/shared/metadata.models'; -import * as uuidv4 from 'uuid/v4'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -184,16 +176,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @Input() model: any; - relationships$: Observable>>; + relationships$: Observable; + relationships: Relationship[]; hasRelationLookup: boolean; modalRef: NgbModalRef; item: Item; listId: string; searchConfig: string; - selectedValues$: Observable, - mdRep: MetadataRepresentation - }>>; + + /** * List of subscriptions to unsubscribe from */ @@ -238,38 +229,54 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo .findById(this.model.submissionId).pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), - switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>) + .pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ) + ) + ); this.subs.push(item$.subscribe((item) => this.item = item)); + this.relationships$ = item$.pipe( + switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType) + .pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((relationshipList: PaginatedList) => relationshipList.page), + startWith([]), + switchMap((relationships: Relationship[]) => + observableCombineLatest( + relationships.map((relationship: Relationship) => + relationship.leftItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((item: Item) => { + return { relationship, left: item.uuid === this.item.uuid } + }), + ) + ))), + map((relationships: { relationship: Relationship, left: boolean }[]) => + relationships + .sort(( + a: { relationship: Relationship, left: boolean }, + b: { relationship: Relationship, left: boolean } + ) => { + const placeA: number = a.left ? a.relationship.leftPlace : a.relationship.rightPlace; + const placeB: number = b.left ? b.relationship.leftPlace : b.relationship.rightPlace; + return Math.sign(placeA - placeB); + }) + .map((relationship) => relationship.relationship) + ) + ) + ) + ); + + this.relationships$.subscribe((rels) => this.relationships = rels); this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), ).subscribe((relatedItems: Array>) => this.selectableListService.select(this.listId, relatedItems)); - - this.relationships$ = this.selectableListService.getSelectableList(this.listId).pipe( - map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []), - ) as Observable>>; - this.selectedValues$ = - observableCombineLatest(item$, this.relationships$).pipe( - map(([item, relatedItems]: [Item, Array>]) => { - return relatedItems - .map((element: SearchResult) => { - const relationMD: MetadataValue = item.firstMetadata(this.model.relationship.metadataField, { value: element.indexableObject.uuid }); - if (hasValue(relationMD)) { - const metadataRepresentationMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: relationMD.authority }); - return { - selectedResult: element, - mdRep: Object.assign( - new ItemMetadataRepresentation(metadataRepresentationMD), - element.indexableObject - ) - }; - } - }).filter(hasValue) - } - ) - ); - } } @@ -327,12 +334,39 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.item = this.item; } - removeSelection(object: SearchResult) { - this.selectableListService.deselectSingle(this.listId, object); - this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType)) - // this.zone.runOutsideAngular( - // () => ); + moveSelection(event: CdkDragDrop) { + moveItemInArray(this.relationships, event.previousIndex, event.currentIndex); + this.zone.runOutsideAngular(() => { + observableCombineLatest( + this.relationships.map((relationship: Relationship, index: number) => + relationship.leftItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((item: Item) => { + const left: boolean = item.uuid === this.item.uuid; + if (left) { + return { relationship, left: item.uuid === this.item.uuid, oldIndex: relationship.leftPlace, newIndex: index } + } else { + return { relationship, left: item.uuid === this.item.uuid, oldIndex: relationship.rightPlace, newIndex: index } + } + }), + ) + ) + ).pipe( + switchMap((relationships: { relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }[]) => + observableCombineLatest(relationships.map((rel: { relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }) => { + if (rel.oldIndex !== rel.newIndex) { + return this.relationshipService.updatePlace(rel.relationship, rel.newIndex, rel.left); + } else { + observableOf(undefined); + } + } + ) + ) + ) + ).subscribe(); + }) } /** @@ -343,4 +377,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); } + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackRelationship(index, relationship: Relationship) { + return hasValue(relationship) ? relationship.id : undefined; + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html new file mode 100644 index 0000000000..ebda014e19 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -0,0 +1,11 @@ +
  • + + + + + +
  • diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..7172653557 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExistingMetadataListElementComponent } from './existing-metadata-list-element.component'; + +describe('ExistingMetadataListElementComponent', () => { + let component: ExistingMetadataListElementComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ExistingMetadataListElementComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExistingMetadataListElementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts new file mode 100644 index 0000000000..4551a0a7d1 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { hasValue, isNotEmpty } from '../../../../empty.util'; +import { filter, map, take } from 'rxjs/operators'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../../app.reducer'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; + +@Component({ + selector: 'ds-existing-metadata-list-element', + templateUrl: './existing-metadata-list-element.component.html', + styleUrls: ['./existing-metadata-list-element.component.scss'] +}) +export class ExistingMetadataListElementComponent implements OnChanges { + @Input() listId: string; + @Input() submissionItem: Item; + @Input() relationship: Relationship; + @Input() metadataFields: string[]; + @Input() relationshipOptions: RelationshipOptions; + metadataRepresentation$; + relatedItem$; + + constructor( + private selectableListService: SelectableListService, + private store: Store + ) { + } + + ngOnChanges() { + const leftItem$ = this.relationship.leftItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ); + + const rightItem$ = this.relationship.rightItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ); + + this.relatedItem$ = observableCombineLatest( + leftItem$, + rightItem$, + ).pipe( + map((items: Item[]) => + items.find((item) => item.uuid !== this.submissionItem.uuid) + ) + ); + + this.metadataRepresentation$ = this.relatedItem$.pipe( + map((relatedItem: Item) => { + console.log(relatedItem); + const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: relatedItem.uuid }); + console.log(relationMD); + if (hasValue(relationMD)) { + const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); + return Object.assign( + new ItemMetadataRepresentation(metadataRepresentationMD), + relatedItem + ) + } + } + ) + ); + } + + removeSelection() { + this.relatedItem$.pipe(take(1)).subscribe((relatedItem: Item) => { + this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: relatedItem })); + this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, relatedItem, this.relationshipOptions.relationshipType)) + }) + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index 3421e6c5c9..9937fb6010 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -2,7 +2,7 @@ import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; import { combineLatest, Observable, Subscription } from 'rxjs'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { hasValue } from '../../../../empty.util'; -import { map, skip, switchMap, take } from 'rxjs/operators'; +import { map, skip, take } from 'rxjs/operators'; import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; @@ -11,15 +11,12 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob import { RelationshipOptions } from '../../models/relationship-options.model'; import { SearchResult } from '../../../../search/search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../../app.reducer'; import { Context } from '../../../../../core/shared/context.model'; -import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; -import { MetadataValue } from '../../../../../core/shared/metadata.models'; @Component({ selector: 'ds-dynamic-lookup-relation-modal', @@ -66,8 +63,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy if (this.relationshipOptions.nameVariants) { this.context = Context.SubmissionModal; } - - // this.setExistingNameVariants(); } close() { @@ -117,37 +112,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ); } - private setExistingNameVariants() { - const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual); - - const relatedItemPairs$: Observable> = - combineLatest(virtualMDs.map((md: MetadataValue) => this.relationshipService.findById(md.virtualValue).pipe(getSucceededRemoteData(), getRemoteDataPayload()))) - .pipe( - switchMap((relationships: Relationship[]) => combineLatest(relationships.map((relationship: Relationship) => - combineLatest( - relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), - relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) - )) - ) - ) - ); - - const relatedItems$: Observable = relatedItemPairs$.pipe( - map(([relatedItemPairs,]: [Array<[Item, Item]>]) => relatedItemPairs.map(([left, right]: [Item, Item]) => left.uuid === this.item.uuid ? left : right)) - ); - - relatedItems$.pipe(take(1)).subscribe((relatedItems) => { - let index = 0; - virtualMDs.forEach( - (md: MetadataValue) => { - this.relationshipService.setNameVariant(this.listId, relatedItems[index].uuid, md.value); - index++; - } - ); - } - ) - } - ngOnDestroy() { Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 85d001286d..7e95d1c727 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -174,6 +174,8 @@ import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.componen import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -196,6 +198,7 @@ const MODULES = [ MomentModule, TextMaskModule, MenuModule, + DragDropModule ]; const ROOT_MODULES = [ @@ -330,7 +333,8 @@ const COMPONENTS = [ ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, - SelectableListItemControlComponent + SelectableListItemControlComponent, + ExistingMetadataListElementComponent ]; const ENTRY_COMPONENTS = [ @@ -432,7 +436,8 @@ const DIRECTIVES = [ ...DIRECTIVES, ...ENTRY_COMPONENTS, ...SHARED_ITEM_PAGE_COMPONENTS, - PublicationSearchResultListElementComponent + PublicationSearchResultListElementComponent, + ExistingMetadataListElementComponent ], providers: [ ...PROVIDERS 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 e40ea82163..0bd24cc304 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -197,21 +197,23 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { find((communities: RemoteData>) => isNotEmpty(communities.payload)), mergeMap((communities: RemoteData>) => communities.payload.page)); - const listCollection$ = communities$.pipe( - flatMap((communityData: Community) => { - return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( - find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), - mergeMap((collections: RemoteData>) => collections.payload.page), - filter((collectionData: Collection) => isNotEmpty(collectionData)), - map((collectionData: Collection) => ({ - communities: [{ id: communityData.id, name: communityData.name }], - collection: { id: collectionData.id, name: collectionData.name } - })) - ); - }), - reduce((acc: any, value: any) => [...acc, ...value], []), - startWith([]) - ); + const listCollection$ = observableOf([]); + + // const listCollection$ = communities$.pipe( + // flatMap((communityData: Community) => { + // return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( + // find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), + // mergeMap((collections: RemoteData>) => collections.payload.page), + // filter((collectionData: Collection) => isNotEmpty(collectionData)), + // map((collectionData: Collection) => ({ + // communities: [{ id: communityData.id, name: communityData.name }], + // collection: { id: collectionData.id, name: collectionData.name } + // })) + // ); + // }), + // reduce((acc: any, value: any) => [...acc, ...value], []), + // startWith([]) + // ); const searchTerm$ = this.searchField.valueChanges.pipe( debounceTime(200), @@ -227,8 +229,8 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { } else { return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); } - }) - ); + }) + ); } } } diff --git a/yarn.lock b/yarn.lock index 26480cccca..884f820c1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,15 @@ dependencies: tslib "^1.9.0" +"@angular/cdk@^7.3.7": + version "7.3.7" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-7.3.7.tgz#ce1ad53ba04beb9c8e950acc5691ea0143753764" + integrity sha512-xbXxhHHKGkVuW6K7pzPmvpJXIwpl0ykBnvA2g+/7Sgy5Pd35wCC+UtHD9RYczDM/mkygNxMQtagyCErwFnDtQA== + dependencies: + tslib "^1.7.1" + optionalDependencies: + parse5 "^5.0.0" + "@angular/cli@^6.1.5": version "6.1.5" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-6.1.5.tgz#312c062631285ff06fd07ecde8afe22cdef5a0e1" @@ -7775,6 +7784,11 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= +parse5@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" + integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + parseqs@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" @@ -10838,6 +10852,11 @@ tsickle@^0.32.1: source-map "^0.6.0" source-map-support "^0.5.0" +tslib@^1.7.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" From edbc32604dc9c168dbb3f998c404f4e096e8e69b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 2 Dec 2019 17:07:28 +0100 Subject: [PATCH 26/77] 67478: LookupRelationService, external source tabs, list implementation --- resources/i18n/en.json5 | 26 ++++- src/app/core/core.module.ts | 2 + src/app/core/data/lookup-relation.service.ts | 94 +++++++++++++++++++ ...try-list-submission-element.component.html | 3 +- ...entry-list-submission-element.component.ts | 13 ++- ...namic-lookup-relation-modal.component.html | 18 +++- ...dynamic-lookup-relation-modal.component.ts | 56 ++++++++++- ...elation-external-source-tab.component.html | 36 ++++--- ...-relation-external-source-tab.component.ts | 7 +- ...-lookup-relation-search-tab.component.html | 6 +- ...ic-lookup-relation-search-tab.component.ts | 28 ++---- ...lookup-relation-selection-tab.component.ts | 12 --- 12 files changed, 239 insertions(+), 62 deletions(-) create mode 100644 src/app/core/data/lookup-relation.service.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index ff5ca4f93e..df0d9b27f0 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1466,6 +1466,8 @@ "search.results.no-results-link": "quotes around it", + "search.results.empty": "Your search returned no results.", + "search.sidebar.close": "Back to results", @@ -1539,13 +1541,21 @@ "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", @@ -1571,6 +1581,14 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e94dc235a7..ea05cb6d83 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -137,6 +137,7 @@ import { SidebarService } from '../shared/sidebar/sidebar.service'; import { NormalizedExternalSource } from './cache/models/normalized-external-source.model'; import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; import { ExternalSourceService } from './data/external-source.service'; +import { LookupRelationService } from './data/lookup-relation.service'; export const restServiceFactory = (cfg: GlobalConfig, mocks: MockResponseMap, http: HttpClient) => { if (ENV_CONFIG.production) { @@ -244,6 +245,7 @@ const PROVIDERS = [ SelectableListService, RelationshipTypeService, ExternalSourceService, + LookupRelationService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts new file mode 100644 index 0000000000..5a36474a66 --- /dev/null +++ b/src/app/core/data/lookup-relation.service.ts @@ -0,0 +1,94 @@ +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { concat, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../shared/item.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Injectable } from '@angular/core'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; + +/** + * A service for retrieving local and external entries information during a relation lookup + */ +@Injectable() +export class LookupRelationService { + /** + * The search config last used for retrieving local results + */ + public searchConfig: PaginatedSearchOptions; + + /** + * Pagination options for retrieving exactly one result + */ + private singleResultOptions = Object.assign(new PaginationComponentOptions(), { + id: 'single-result-options', + pageSize: 1 + }); + + constructor(protected externalSourceService: ExternalSourceService, + protected searchService: SearchService) { + } + + /** + * Retrieve the available local entries for a relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + * @param setSearchConfig Optionally choose if we should store the used search config in a local variable (defaults to true) + */ + getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { + const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + ); + if (setSearchConfig) { + this.searchConfig = newConfig; + } + return this.searchService.search(newConfig).pipe( + /* Make sure to only listen to the first x results, until loading is finished */ + /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ + multicast( + () => new ReplaySubject(1), + (subject) => subject.pipe( + takeWhile((rd: RemoteData>>) => rd.isLoading), + concat(subject.pipe(take(1))) + ) + ) as any + ) + } + + /** + * Calculate the total local entries available for the given relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + */ + getTotalLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions): Observable { + return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList>) => results.totalElements), + startWith(0) + ); + } + + /** + * Calculate the total external entries available for a given external source + * @param externalSource External Source + * @param searchOptions Search options to filter results + */ + getTotalExternalResults(externalSource: ExternalSource, searchOptions: PaginatedSearchOptions): Observable { + return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList) => results.totalElements), + startWith(0) + ); + } +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html index 785d0dd121..5595be6b7d 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -1 +1,2 @@ -
    Listable external source works! Display: {{object.value}}
    +
    {{object.display}}
    + diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index 7a6b051451..673370cc2c 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -3,7 +3,9 @@ import { ExternalSourceEntry } from '../../../../../core/shared/external-source- import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { Context } from '../../../../../core/shared/context.model'; -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; @listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) @Component({ @@ -11,6 +13,13 @@ import { Component } from '@angular/core'; styleUrls: ['./external-source-entry-list-submission-element.component.scss'], templateUrl: './external-source-entry-list-submission-element.component.html' }) -export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent { +export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { + /** + * The metadata value for the object's uri + */ + uri: MetadataValue; + ngOnInit(): void { + this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 52f983e723..46620aa00b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -7,7 +7,7 @@
    -
    \ No newline at end of file +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index 3421e6c5c9..39f3e12bb9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -1,5 +1,5 @@ import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest, Observable, Subscription } from 'rxjs'; +import { combineLatest, Observable, Subscription, zip as observableZip } from 'rxjs'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { hasValue } from '../../../../empty.util'; import { map, skip, switchMap, take } from 'rxjs/operators'; @@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob import { RelationshipOptions } from '../../models/relationship-options.model'; import { SearchResult } from '../../../../search/search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../../core/shared/operators'; import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; @@ -20,6 +24,11 @@ import { AppState } from '../../../../../app.reducer'; import { Context } from '../../../../../core/shared/context.model'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { ExternalSource } from '../../../../../core/shared/external-source.model'; +import { ExternalSourceService } from '../../../../../core/data/external-source.service'; @Component({ selector: 'ds-dynamic-lookup-relation-modal', @@ -46,11 +55,29 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy [uuid: string]: Subscription } = {}; + /** + * A list of the available external sources configured for this relationship + */ + externalSourcesRD$: Observable>>; + + /** + * The total amount of internal items for the current options + */ + totalInternal$: Observable; + + /** + * The total amount of results for each external source using the current options + */ + totalExternal$: Observable; + constructor( public modal: NgbActiveModal, private selectableListService: SelectableListService, private relationshipService: RelationshipService, private relationshipTypeService: RelationshipTypeService, + private externalSourceService: ExternalSourceService, + private lookupRelationService: LookupRelationService, + private searchConfigService: SearchConfigurationService, private zone: NgZone, private store: Store ) { @@ -67,6 +94,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy this.context = Context.SubmissionModal; } + this.externalSourcesRD$ = this.externalSourceService.findAll(); + + this.setTotals(); // this.setExistingNameVariants(); } @@ -148,6 +178,28 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ) } + /** + * Calculate and set the total entries available for each tab + */ + setTotals() { + this.totalInternal$ = this.searchConfigService.paginatedSearchOptions.pipe( + switchMap((options) => this.lookupRelationService.getTotalLocalResults(this.relationshipOptions, options)) + ); + + const externalSourcesAndOptions$ = combineLatest( + this.externalSourcesRD$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ), + this.searchConfigService.paginatedSearchOptions + ); + + this.totalExternal$ = externalSourcesAndOptions$.pipe( + switchMap(([sources, options]) => + observableZip(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) + ); + } + ngOnDestroy() { Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html index 5105548306..6b66be97c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -4,20 +4,28 @@
    -
    - {{'submission.sections.describe.relationship-lookup.selection-tab.no-selection' | translate}} -
    -
    -

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + label | translate}}

    - + +
    +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

    + + + + + +
    + {{ 'search.results.empty' | translate }} +
    +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index c138e4c1ae..aae7b8b2f1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -10,9 +10,10 @@ import { ExternalSourceEntry } from '../../../../../../core/shared/external-sour import { ExternalSource } from '../../../../../../core/shared/external-source.model'; import { switchMap } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; -import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { Context } from '../../../../../../core/shared/context.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { fadeIn, fadeInOut } from '../../../../../animations/fade'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -23,6 +24,10 @@ import { ListableObject } from '../../../../../object-collection/shared/listable provide: SEARCH_CONFIG_SERVICE, useClass: SearchConfigurationService } + ], + animations: [ + fadeIn, + fadeInOut ] }) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index 4e2da1f12b..f5b9a86758 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -56,8 +56,8 @@ - \ No newline at end of file + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index c7bb7104b5..40d64ca939 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -11,7 +11,7 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { SearchService } from '../../../../../../core/shared/search/search.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators'; @@ -20,6 +20,7 @@ import { getSucceededRemoteData } from '../../../../../../core/shared/operators' import { RouteService } from '../../../../../../core/services/route.service'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; import { Context } from '../../../../../../core/shared/context.model'; +import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; @Component({ selector: 'ds-dynamic-lookup-relation-search-tab', @@ -43,7 +44,6 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); resultsRD$: Observable>>>; - searchConfig: PaginatedSearchOptions; allSelected: boolean; someSelected$: Observable; selectAllLoading: boolean; @@ -57,9 +57,11 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest constructor( private searchService: SearchService, private router: Router, + private route: ActivatedRoute, private selectableListService: SelectableListService, private searchConfigService: SearchConfigurationService, private routeService: RouteService, + protected lookupRelationService: LookupRelationService ) { } @@ -70,29 +72,13 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection))); this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( - map((options) => { - return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration }) - }), - switchMap((options) => { - this.searchConfig = options; - return this.searchService.search(options).pipe( - /* Make sure to only listen to the first x results, until loading is finished */ - /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ - multicast( - () => new ReplaySubject(1), - (subject) => subject.pipe( - takeWhile((rd: RemoteData>>) => rd.isLoading), - concat(subject.pipe(take(1))) - ) - ) as any - ) - }) + switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options)) ); } resetRoute() { this.router.navigate([], { - queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), + queryParams: Object.assign({}, { pageSize: this.initialPagination.pageSize }, this.route.snapshot.queryParams, { page: 1 }) }); } @@ -124,7 +110,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest currentPage: 1, pageSize: 9999 }); - const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination }); + const fullSearchConfig = Object.assign(this.lookupRelationService.searchConfig, { pagination: fullPagination }); const results$ = this.searchService.search(fullSearchConfig) as Observable>>>; results$.pipe( getSucceededRemoteData(), diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index b47207a957..3dcde9ea8e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -35,17 +35,11 @@ export class DsDynamicLookupRelationSelectionTabComponent { @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); - initialPagination = Object.assign(new PaginationComponentOptions(), { - id: 'submission-relation-list', - pageSize: 5 - }); - constructor(private router: Router, private searchConfigService: SearchConfigurationService) { } ngOnInit() { - this.resetRoute(); this.selectionRD$ = this.searchConfigService.paginatedSearchOptions .pipe( map((options: PaginatedSearchOptions) => options.pagination), @@ -69,10 +63,4 @@ export class DsDynamicLookupRelationSelectionTabComponent { }) ) } - - resetRoute() { - this.router.navigate([], { - queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), - }); - } } From 78d1c5ee2bfcc353b7ae452ab1ac3aa3c4ee65c3 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Mon, 2 Dec 2019 18:30:35 +0100 Subject: [PATCH 27/77] use reorderables instead of relationships --- ...on-item-metadata-list-element.component.ts | 7 +- ...amic-form-control-container.component.html | 4 +- ...ynamic-form-control-container.component.ts | 65 ++++----- ...sting-metadata-list-element.component.html | 4 +- ...xisting-metadata-list-element.component.ts | 127 +++++++++++------- 5 files changed, 119 insertions(+), 88 deletions(-) diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts index f3d0a28fda..b571348122 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator'; import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; @@ -11,5 +11,8 @@ import { ItemMetadataRepresentationListElementComponent } from '../../../../shar /** * The component for displaying an item of the type Person as a metadata field */ -export class PersonItemMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent { +export class PersonItemMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent implements OnInit { + ngOnInit(): void { + console.log('this.metadataRepresentation', this.metadataRepresentation); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 64f8315575..d2a1996a60 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -54,8 +54,8 @@
      - ; - relationships: Relationship[]; + reorderables$: Observable; + reorderables: ReorderableRelationship[]; hasRelationLookup: boolean; modalRef: NgbModalRef; item: Item; listId: string; searchConfig: string; - /** * List of subscriptions to unsubscribe from */ @@ -223,6 +226,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo ngOnInit(): void { this.hasRelationLookup = hasValue(this.model.relationship); + this.reorderables = []; if (this.hasRelationLookup) { this.listId = 'list-' + this.model.relationship.relationshipType; const item$ = this.submissionObjectService @@ -238,7 +242,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo ); this.subs.push(item$.subscribe((item) => this.item = item)); - this.relationships$ = item$.pipe( + this.reorderables$ = item$.pipe( switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType) .pipe( getAllSucceededRemoteData(), @@ -251,28 +255,24 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo relationship.leftItem.pipe( getSucceededRemoteData(), getRemoteDataPayload(), - map((item: Item) => { - return { relationship, left: item.uuid === this.item.uuid } + map((leftItem: Item) => { + return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid) }), ) ))), - map((relationships: { relationship: Relationship, left: boolean }[]) => - relationships - .sort(( - a: { relationship: Relationship, left: boolean }, - b: { relationship: Relationship, left: boolean } - ) => { - const placeA: number = a.left ? a.relationship.leftPlace : a.relationship.rightPlace; - const placeB: number = b.left ? b.relationship.leftPlace : b.relationship.rightPlace; - return Math.sign(placeA - placeB); - }) - .map((relationship) => relationship.relationship) - ) + map((relationships: ReorderableRelationship[]) => + relationships + .sort((a: Reorderable, b: Reorderable) => { + return Math.sign(a.getPlace() - b.getPlace()); + }) + ) ) ) ); - this.relationships$.subscribe((rels) => this.relationships = rels); + this.subs.push(this.reorderables$.subscribe((rs) => { + this.reorderables = rs + })); this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), @@ -334,27 +334,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.item = this.item; } - moveSelection(event: CdkDragDrop) { - moveItemInArray(this.relationships, event.previousIndex, event.currentIndex); + moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); this.zone.runOutsideAngular(() => { observableCombineLatest( - this.relationships.map((relationship: Relationship, index: number) => - relationship.leftItem.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((item: Item) => { - const left: boolean = item.uuid === this.item.uuid; - if (left) { - return { relationship, left: item.uuid === this.item.uuid, oldIndex: relationship.leftPlace, newIndex: index } - } else { - return { relationship, left: item.uuid === this.item.uuid, oldIndex: relationship.rightPlace, newIndex: index } - } - }), - ) + this.reorderables.map((reo: Reorderable, index: number) => { + reo.oldIndex = reo.getPlace(); + reo.newIndex = index; + return reo; + } ) ).pipe( - switchMap((relationships: { relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }[]) => + switchMap((relationships: Array<{ relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }>) => observableCombineLatest(relationships.map((rel: { relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }) => { if (rel.oldIndex !== rel.newIndex) { return this.relationshipService.updatePlace(rel.relationship, rel.newIndex, rel.left); @@ -381,7 +372,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo /** * Prevent unnecessary rerendering so fields don't lose focus */ - trackRelationship(index, relationship: Relationship) { - return hasValue(relationship) ? relationship.id : undefined; + trackReorderable(index, reorderable: Reorderable) { + return hasValue(reorderable) ? reorderable.getId() : undefined; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html index ebda014e19..960dd78767 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -1,4 +1,4 @@ -
    • +
    • @@ -6,6 +6,6 @@ - +
    • diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index 4551a0a7d1..d31e9380a8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -1,10 +1,16 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../empty.util'; -import { filter, map, take } from 'rxjs/operators'; +import { of as observableOf, Subscription } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; -import { combineLatest as observableCombineLatest } from 'rxjs'; +import { combineLatest as observableCombineLatest, of } from 'rxjs'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; @@ -14,19 +20,55 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../../../../app.reducer'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +export abstract class Reorderable { + constructor(public oldIndex?: number, public newIndex?: number) { + } + + abstract getId(): string; + abstract getPlace(): number; +} + +export class ReorderableRelationship extends Reorderable { + relationship: Relationship; + useLeftItem: boolean; + + constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) { + super(oldIndex, newIndex); + this.relationship = relationship; + this.useLeftItem = useLeftItem; + } + + getId(): string { + return this.relationship.id; + } + + getPlace(): number { + if (this.useLeftItem) { + return this.relationship.rightPlace + } else { + return this.relationship.leftPlace + } + } +} + @Component({ selector: 'ds-existing-metadata-list-element', templateUrl: './existing-metadata-list-element.component.html', styleUrls: ['./existing-metadata-list-element.component.scss'] }) -export class ExistingMetadataListElementComponent implements OnChanges { +export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy { @Input() listId: string; @Input() submissionItem: Item; - @Input() relationship: Relationship; + @Input() reoRel: ReorderableRelationship; @Input() metadataFields: string[]; @Input() relationshipOptions: RelationshipOptions; - metadataRepresentation$; - relatedItem$; + metadataRepresentation: MetadataRepresentation; + relatedItem: Item; + + /** + * List of subscriptions to unsubscribe from + */ + private subs: Subscription[] = []; constructor( private selectableListService: SelectableListService, @@ -34,49 +76,44 @@ export class ExistingMetadataListElementComponent implements OnChanges { ) { } + ngOnInit(): void { + console.log('reoRel', this.reoRel); + } + ngOnChanges() { - const leftItem$ = this.relationship.leftItem.pipe( - getSucceededRemoteData(), + const item$ = this.reoRel.useLeftItem ? + this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; + + this.subs.push(item$.pipe( + getAllSucceededRemoteData(), getRemoteDataPayload(), filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) - ); + ).subscribe((item: Item) => { + this.relatedItem = item; - const rightItem$ = this.relationship.rightItem.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) - ); - - this.relatedItem$ = observableCombineLatest( - leftItem$, - rightItem$, - ).pipe( - map((items: Item[]) => - items.find((item) => item.uuid !== this.submissionItem.uuid) - ) - ); - - this.metadataRepresentation$ = this.relatedItem$.pipe( - map((relatedItem: Item) => { - console.log(relatedItem); - const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: relatedItem.uuid }); - console.log(relationMD); - if (hasValue(relationMD)) { - const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); - return Object.assign( - new ItemMetadataRepresentation(metadataRepresentationMD), - relatedItem - ) - } - } - ) - ); + const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); + if (hasValue(relationMD)) { + const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); + this.metadataRepresentation = Object.assign( + new ItemMetadataRepresentation(metadataRepresentationMD), + this.relatedItem + ) + } + })); } removeSelection() { - this.relatedItem$.pipe(take(1)).subscribe((relatedItem: Item) => { - this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: relatedItem })); - this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, relatedItem, this.relationshipOptions.relationshipType)) - }) + this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem })); + this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType)) } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + } From c3ef2f8dee688587ce5ce7962e2f39785e16ab27 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 3 Dec 2019 10:10:46 +0100 Subject: [PATCH 28/77] fixed reordering request --- src/app/core/data/data.service.spec.ts | 4 +- ...ynamic-form-control-container.component.ts | 43 ++++++++----------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 18e008fc0e..b690492c61 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -17,6 +17,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { NotificationsService } from '../../shared/notifications/notifications.service'; import { Item } from '../shared/item.model'; import * as uuidv4 from 'uuid/v4'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; const endpoint = 'https://rest.api/core'; @@ -191,8 +192,7 @@ describe('DataService', () => { dso2.self = selfLink; dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(service, 'findByHref').and.returnValues(observableOf(dso)); - spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso)); + spyOn(service, 'findByHref').and.returnValues(createSuccessfulRemoteDataObject$(dso)); spyOn(objectCache, 'addPatch'); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 8032cd606a..0cf939623e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -260,12 +260,12 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo }), ) ))), - map((relationships: ReorderableRelationship[]) => - relationships - .sort((a: Reorderable, b: Reorderable) => { - return Math.sign(a.getPlace() - b.getPlace()); - }) - ) + map((relationships: ReorderableRelationship[]) => + relationships + .sort((a: Reorderable, b: Reorderable) => { + return Math.sign(a.getPlace() - b.getPlace()); + }) + ) ) ) ); @@ -337,25 +337,20 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo moveSelection(event: CdkDragDrop) { moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); this.zone.runOutsideAngular(() => { - observableCombineLatest( - this.reorderables.map((reo: Reorderable, index: number) => { - reo.oldIndex = reo.getPlace(); - reo.newIndex = index; - return reo; + const relationships = this.reorderables.map((reo: Reorderable, index: number) => { + reo.oldIndex = reo.getPlace(); + reo.newIndex = index; + return reo; + } + ); + return observableCombineLatest(relationships.map((rel: ReorderableRelationship) => { + console.log(rel); + if (rel.oldIndex !== rel.newIndex) { + return this.relationshipService.updatePlace(rel.relationship, rel.newIndex, !rel.useLeftItem); + } else { + return observableOf(undefined); } - ) - ).pipe( - switchMap((relationships: Array<{ relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }>) => - observableCombineLatest(relationships.map((rel: { relationship: Relationship, left: boolean, oldIndex: number, newIndex: number }) => { - if (rel.oldIndex !== rel.newIndex) { - return this.relationshipService.updatePlace(rel.relationship, rel.newIndex, rel.left); - } else { - observableOf(undefined); - } - } - ) - ) - ) + }) ).subscribe(); }) } From c48932431f00ca9cb35765077fc2d933bd889dd2 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 3 Dec 2019 11:43:19 +0100 Subject: [PATCH 29/77] fixes after merge + tslint changes --- src/app/+home-page/home-page.component.ts | 6 ++-- src/app/+home-page/home-page.resolver.ts | 4 +-- src/app/+lookup-by-id/lookup-guard.ts | 2 +- .../filtered-search-page.component.spec.ts | 2 +- .../+search-page/search-tracker.component.ts | 32 +++++++++-------- src/app/+search-page/search.component.html | 6 +++- src/app/+search-page/search.component.ts | 15 ++++---- src/app/core/auth/auth.interceptor.ts | 2 +- src/app/core/data/change-analyzer.ts | 2 +- src/app/core/data/site-data.service.spec.ts | 14 ++++---- src/app/core/data/site-data.service.ts | 34 ++++++++----------- src/app/core/shared/search/search.service.ts | 24 ++++++------- ...xisting-metadata-list-element.component.ts | 11 ++---- .../lang-switch/lang-switch.component.spec.ts | 2 +- .../shared/mocks/mock-angulartics.service.ts | 2 +- .../search-facet-filter.component.ts | 3 +- .../search-label/search-label.component.ts | 7 ++-- ...idebar-filter-selected-option.component.ts | 4 +-- .../sidebar/filter/sidebar-filter.actions.ts | 2 +- .../filter/sidebar-filter.component.ts | 22 ++++++------ .../sidebar/filter/sidebar-filter.reducer.ts | 8 ++--- .../sidebar/filter/sidebar-filter.service.ts | 22 ++++++------ .../page-with-sidebar.component.spec.ts | 9 +++-- .../sidebar/page-with-sidebar.component.ts | 22 ++++++------ .../sidebar/sidebar-dropdown.component.ts | 6 ++-- src/app/shared/utils/object-keys-pipe.ts | 2 +- src/app/shared/utils/object-values-pipe.ts | 2 +- .../angulartics/dspace-provider.spec.ts | 6 ++-- .../statistics/angulartics/dspace-provider.ts | 6 ++-- .../dspace/view-tracker.component.ts | 6 ++-- src/app/statistics/statistics.module.ts | 2 +- src/app/statistics/statistics.service.spec.ts | 21 ++++++------ src/app/statistics/statistics.service.ts | 22 ++++++------ tslint.json | 7 ++++ 34 files changed, 170 insertions(+), 167 deletions(-) diff --git a/src/app/+home-page/home-page.component.ts b/src/app/+home-page/home-page.component.ts index 1b915ae683..65caa01430 100644 --- a/src/app/+home-page/home-page.component.ts +++ b/src/app/+home-page/home-page.component.ts @@ -11,14 +11,14 @@ import { Site } from '../core/shared/site.model'; }) export class HomePageComponent implements OnInit { - site$:Observable; + site$: Observable; constructor( - private route:ActivatedRoute, + private route: ActivatedRoute, ) { } - ngOnInit():void { + ngOnInit(): void { this.site$ = this.route.data.pipe( map((data) => data.site as Site), ); diff --git a/src/app/+home-page/home-page.resolver.ts b/src/app/+home-page/home-page.resolver.ts index 1145d1d013..6b63a4e782 100644 --- a/src/app/+home-page/home-page.resolver.ts +++ b/src/app/+home-page/home-page.resolver.ts @@ -10,7 +10,7 @@ import { take } from 'rxjs/operators'; */ @Injectable() export class HomePageResolver implements Resolve { - constructor(private siteService:SiteDataService) { + constructor(private siteService: SiteDataService) { } /** @@ -19,7 +19,7 @@ export class HomePageResolver implements Resolve { * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns Observable Emits the found Site object, or an error if something went wrong */ - resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable | Promise | Site { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | Site { return this.siteService.find().pipe(take(1)); } } diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index c89e329241..a7ddffcd4e 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -18,7 +18,7 @@ export class LookupGuard implements CanActivate { constructor(private dsoService: DsoRedirectDataService) { } - canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = this.getLookupParams(route); return this.dsoService.findById(params.id, params.type).pipe( map((response: RemoteData) => response.hasFailed) diff --git a/src/app/+search-page/filtered-search-page.component.spec.ts b/src/app/+search-page/filtered-search-page.component.spec.ts index cf1668d8bc..e25cbd2e12 100644 --- a/src/app/+search-page/filtered-search-page.component.spec.ts +++ b/src/app/+search-page/filtered-search-page.component.spec.ts @@ -1,7 +1,7 @@ import { FilteredSearchPageComponent } from './filtered-search-page.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { configureSearchComponentTestingModule } from './search.component.spec'; -import { SearchConfigurationService } from './search-service/search-configuration.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; describe('FilteredSearchPageComponent', () => { let comp: FilteredSearchPageComponent; diff --git a/src/app/+search-page/search-tracker.component.ts b/src/app/+search-page/search-tracker.component.ts index e1df9b3905..7e5aa49165 100644 --- a/src/app/+search-page/search-tracker.component.ts +++ b/src/app/+search-page/search-tracker.component.ts @@ -2,16 +2,17 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; import { filter, map, switchMap } from 'rxjs/operators'; import { SearchComponent } from './search.component'; -import { SearchService } from './search-service/search.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { SearchConfigurationService } from './search-service/search-configuration.service'; import { HostWindowService } from '../shared/host-window.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { RouteService } from '../core/services/route.service'; import { hasValue } from '../shared/empty.util'; -import { SearchQueryResponse } from './search-service/search-query-response.model'; import { SearchSuccessResponse } from '../core/cache/response.models'; -import { PaginatedSearchOptions } from './paginated-search-options.model'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; +import { SearchService } from '../core/shared/search/search.service'; +import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; +import { SearchQueryResponse } from '../shared/search/search-query-response.model'; +import { Router } from '@angular/router'; /** * This component triggers a page view statistic @@ -30,17 +31,18 @@ import { PaginatedSearchOptions } from './paginated-search-options.model'; export class SearchTrackerComponent extends SearchComponent implements OnInit { constructor( - protected service:SearchService, - protected sidebarService:SidebarService, - protected windowService:HostWindowService, - @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService, - protected routeService:RouteService, - public angulartics2:Angulartics2 + protected service: SearchService, + protected sidebarService: SidebarService, + protected windowService: HostWindowService, + @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, + protected routeService: RouteService, + public angulartics2: Angulartics2, + protected router: Router ) { - super(service, sidebarService, windowService, searchConfigService, routeService); + super(service, sidebarService, windowService, searchConfigService, routeService, router); } - ngOnInit():void { + ngOnInit(): void { // super.ngOnInit(); this.getSearchOptions().pipe( switchMap((options) => this.service.searchEntries(options) @@ -58,9 +60,9 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { ) ) .subscribe((entry) => { - const config:PaginatedSearchOptions = entry.searchOptions; - const searchQueryResponse:SearchQueryResponse = entry.response; - const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = []; + const config: PaginatedSearchOptions = entry.searchOptions; + const searchQueryResponse: SearchQueryResponse = entry.response; + const filters: Array<{ filter: string, operator: string, value: string, label: string; }> = []; const appliedFilters = searchQueryResponse.appliedFilters || []; for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { const appliedFilter = appliedFilters[i]; diff --git a/src/app/+search-page/search.component.html b/src/app/+search-page/search.component.html index 9423062bde..36879f33d4 100644 --- a/src/app/+search-page/search.component.html +++ b/src/app/+search-page/search.component.html @@ -46,5 +46,9 @@ [scopes]="(scopeListRD$ | async)" [inPlaceSearch]="inPlaceSearch"> - +
      +
      + +
      +
      diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index cc763e253f..5b5787c91f 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -6,15 +6,17 @@ import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { pushInOut } from '../shared/animations/push'; import { HostWindowService } from '../shared/host-window.service'; -import { PaginatedSearchOptions } from './paginated-search-options.model'; -import { SearchResult } from './search-result.model'; -import { SearchService } from './search-service/search.service'; import { SidebarService } from '../shared/sidebar/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 '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; +import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; +import { SearchResult } from '../shared/search/search-result.model'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; +import { SearchService } from '../core/shared/search/search.service'; +import { currentPath } from '../shared/utils/route.utils'; +import { Router } from '@angular/router'; @Component({ selector: 'ds-search', @@ -96,7 +98,8 @@ export class SearchComponent implements OnInit { protected sidebarService: SidebarService, protected windowService: HostWindowService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, - protected routeService: RouteService) { + protected routeService: RouteService, + protected router: Router) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -159,7 +162,7 @@ export class SearchComponent implements OnInit { */ private getSearchLink(): string { if (this.inPlaceSearch) { - return './'; + return currentPath(this.router); } return this.service.getSearchLink(); } diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index da760b8faa..08e892bbd9 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -56,7 +56,7 @@ export class AuthInterceptor implements HttpInterceptor { return http.url && http.url.endsWith('/authn/logout'); } - private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus { + private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus { const authStatus = new AuthStatus(); authStatus.id = null; authStatus.okay = true; diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts index 6b5a69259b..c45c9e55b7 100644 --- a/src/app/core/data/change-analyzer.ts +++ b/src/app/core/data/change-analyzer.ts @@ -17,5 +17,5 @@ export interface ChangeAnalyzer { * @param {NormalizedObject} object2 * The second object to compare */ - diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[]; + diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[]; } diff --git a/src/app/core/data/site-data.service.spec.ts b/src/app/core/data/site-data.service.spec.ts index 189218b5cf..3059ab9948 100644 --- a/src/app/core/data/site-data.service.spec.ts +++ b/src/app/core/data/site-data.service.spec.ts @@ -19,12 +19,12 @@ import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; describe('SiteDataService', () => { - let scheduler:TestScheduler; - let service:SiteDataService; - let halService:HALEndpointService; - let requestService:RequestService; - let rdbService:RemoteDataBuildService; - let objectCache:ObjectCacheService; + let scheduler: TestScheduler; + let service: SiteDataService; + let halService: HALEndpointService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; const testObject = Object.assign(new Site(), { uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746', @@ -33,7 +33,7 @@ describe('SiteDataService', () => { const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; const options = Object.assign(new FindAllOptions(), {}); - const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => { + const getRequestEntry$ = (successful: boolean, statusCode: number, statusText: string) => { return observableOf({ response: new RestResponse(successful, statusCode, statusText) } as RequestEntry); diff --git a/src/app/core/data/site-data.service.ts b/src/app/core/data/site-data.service.ts index 4993d47226..ba395b40ed 100644 --- a/src/app/core/data/site-data.service.ts +++ b/src/app/core/data/site-data.service.ts @@ -22,47 +22,41 @@ import { getSucceededRemoteData } from '../shared/operators'; * Service responsible for handling requests related to the Site object */ @Injectable() -export class SiteDataService extends DataService { -​ +export class SiteDataService extends DataService {​ protected linkPath = 'sites'; protected forceBypassCache = false; -​ constructor( - protected requestService:RequestService, - protected rdbService:RemoteDataBuildService, - protected dataBuildService:NormalizedObjectBuildService, - protected store:Store, - protected objectCache:ObjectCacheService, - protected halService:HALEndpointService, - protected notificationsService:NotificationsService, - protected http:HttpClient, - protected comparator:DSOChangeAnalyzer, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, ) { super(); } -​ - /** * Get the endpoint for browsing the site object * @param {FindAllOptions} options * @param {Observable} linkPath */ - getBrowseEndpoint(options:FindAllOptions, linkPath?:string):Observable { + getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable { return this.halService.getEndpoint(this.linkPath); } -​ - /** * Retrieve the Site Object */ - find():Observable { + find(): Observable { return this.findAll().pipe( getSucceededRemoteData(), - map((remoteData:RemoteData>) => remoteData.payload), - map((list:PaginatedList) => list.page[0]) + map((remoteData: RemoteData>) => remoteData.payload), + map((list: PaginatedList) => list.page[0]) ); } } diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index f6886c268e..141f261990 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -89,9 +89,9 @@ export class SearchService implements OnDestroy { } } - getEndpoint(searchOptions?:PaginatedSearchOptions):Observable { + getEndpoint(searchOptions?: PaginatedSearchOptions): Observable { return this.halService.getEndpoint(this.searchLinkPath).pipe( - map((url:string) => { + map((url: string) => { if (hasValue(searchOptions)) { return (searchOptions as PaginatedSearchOptions).toRestUrl(url); } else { @@ -117,16 +117,15 @@ export class SearchService implements OnDestroy { * @param responseMsToLive The amount of milliseconds for the response to live in cache * @returns {Observable} Emits an observable with the request entries */ - searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number) - :Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> { + searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> { const hrefObs = this.getEndpoint(searchOptions); const requestObs = hrefObs.pipe( - map((url:string) => { + map((url: string) => { const request = new this.request(this.requestService.generateRequestId(), url); - const getResponseParserFn:() => GenericConstructor = () => { + const getResponseParserFn: () => GenericConstructor = () => { return this.parser; }; @@ -139,8 +138,8 @@ export class SearchService implements OnDestroy { configureRequest(this.requestService), ); return requestObs.pipe( - switchMap((request:RestRequest) => this.requestService.getByHref(request.href)), - map(((requestEntry:RequestEntry) => ({ + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)), + map(((requestEntry: RequestEntry) => ({ searchOptions: searchOptions, requestEntry: requestEntry }))) @@ -152,16 +151,15 @@ export class SearchService implements OnDestroy { * @param searchEntries: The request entries from the search method * @returns {Observable>>>} Emits a paginated list with all search results found */ - getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>) - :Observable>>> { - const requestEntryObs:Observable = searchEntries.pipe( + getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable>>> { + const requestEntryObs: Observable = searchEntries.pipe( map((entry) => entry.requestEntry), ); // get search results from response cache - const sqrObs:Observable = requestEntryObs.pipe( + const sqrObs: Observable = requestEntryObs.pipe( filterSuccessfulResponses(), - map((response:SearchSuccessResponse) => response.results), + map((response: SearchSuccessResponse) => response.results), ); // turn dspace href from search results to effective list of DSpaceObjects diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index d31e9380a8..def879701e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -1,16 +1,11 @@ import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model'; -import { - getAllSucceededRemoteData, - getRemoteDataPayload, - getSucceededRemoteData -} from '../../../../../core/shared/operators'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../empty.util'; -import { of as observableOf, Subscription } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; -import { combineLatest as observableCombineLatest, of } from 'rxjs'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; diff --git a/src/app/shared/lang-switch/lang-switch.component.spec.ts b/src/app/shared/lang-switch/lang-switch.component.spec.ts index 5b10578f77..3d7aca46b6 100644 --- a/src/app/shared/lang-switch/lang-switch.component.spec.ts +++ b/src/app/shared/lang-switch/lang-switch.component.spec.ts @@ -78,7 +78,7 @@ describe('LangSwitchComponent', () => { }).compileComponents() .then(() => { translate = TestBed.get(TranslateService); - translate.addLangs(mockConfig.languages.filter((langConfig:LangConfig) => langConfig.active === true).map((a) => a.code)); + translate.addLangs(mockConfig.languages.filter((langConfig: LangConfig) => langConfig.active === true).map((a) => a.code)); translate.setDefaultLang('en'); translate.use('en'); http = TestBed.get(HttpTestingController); diff --git a/src/app/shared/mocks/mock-angulartics.service.ts b/src/app/shared/mocks/mock-angulartics.service.ts index 5581e183d1..a7516eb44a 100644 --- a/src/app/shared/mocks/mock-angulartics.service.ts +++ b/src/app/shared/mocks/mock-angulartics.service.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-empty */ export class AngularticsMock { public eventTrack(action, properties) { } - public startTracking():void {} + public startTracking(): void {} } diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 3bed8e7397..1d6a85b95b 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -24,6 +24,7 @@ import { getSucceededRemoteData } from '../../../../../core/shared/operators'; import { InputSuggestion } from '../../../../input-suggestions/input-suggestions.model'; import { SearchOptions } from '../../../search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; +import { currentPath } from '../../../../utils/route.utils'; @Component({ selector: 'ds-search-facet-filter', @@ -185,7 +186,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { */ public getSearchLink(): string { if (this.inPlaceSearch) { - return ''; + return currentPath(this.router); } return this.searchService.getSearchLink(); } diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.ts b/src/app/shared/search/search-labels/search-label/search-label.component.ts index e821af19c9..bc3a8b2897 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.ts @@ -1,9 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { Params } from '@angular/router'; +import { Params, Router } from '@angular/router'; import { map } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../../empty.util'; import { SearchService } from '../../../../core/shared/search/search.service'; +import { currentPath } from '../../../utils/route.utils'; @Component({ selector: 'ds-search-label', @@ -25,7 +26,7 @@ export class SearchLabelComponent implements OnInit { * Initialize the instance variable */ constructor( - private searchService: SearchService) { + private searchService: SearchService, private router: Router) { } ngOnInit(): void { @@ -55,7 +56,7 @@ export class SearchLabelComponent implements OnInit { */ private getSearchLink(): string { if (this.inPlaceSearch) { - return './'; + return currentPath(this.router); } return this.searchService.getSearchLink(); } diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts index 5c80a9cd87..4f1d2415ae 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts @@ -10,6 +10,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; * Represents a single selected option in a sidebar filter */ export class SidebarFilterSelectedOptionComponent { - @Input() label:string; - @Output() click:EventEmitter = new EventEmitter(); + @Input() label: string; + @Output() click: EventEmitter = new EventEmitter(); } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts index 2391274489..644bebd949 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts @@ -45,7 +45,7 @@ export class FilterInitializeAction extends SidebarFilterAction { type = SidebarFilterActionTypes.INITIALIZE; initiallyExpanded; - constructor(name:string, initiallyExpanded:boolean) { + constructor(name: string, initiallyExpanded: boolean) { super(name); this.initiallyExpanded = initiallyExpanded; } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.ts b/src/app/shared/sidebar/filter/sidebar-filter.component.ts index 2a98565639..5a019d41df 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.ts @@ -15,13 +15,13 @@ import { slide } from '../../animations/slide'; */ export class SidebarFilterComponent implements OnInit { - @Input() name:string; - @Input() type:string; - @Input() label:string; + @Input() name: string; + @Input() type: string; + @Input() label: string; @Input() expanded = true; @Input() singleValue = false; - @Input() selectedValues:Observable; - @Output() removeValue:EventEmitter = new EventEmitter(); + @Input() selectedValues: Observable; + @Output() removeValue: EventEmitter = new EventEmitter(); /** * True when the filter is 100% collapsed in the UI @@ -31,10 +31,10 @@ export class SidebarFilterComponent implements OnInit { /** * Emits true when the filter is currently collapsed in the store */ - collapsed$:Observable; + collapsed$: Observable; constructor( - protected filterService:SidebarFilterService, + protected filterService: SidebarFilterService, ) { } @@ -49,7 +49,7 @@ export class SidebarFilterComponent implements OnInit { * Method to change this.collapsed to false when the slide animation ends and is sliding open * @param event The animation event */ - finishSlide(event:any):void { + finishSlide(event: any): void { if (event.fromState === 'collapsed') { this.closed = false; } @@ -59,13 +59,13 @@ export class SidebarFilterComponent implements OnInit { * Method to change this.collapsed to true when the slide animation starts and is sliding closed * @param event The animation event */ - startSlide(event:any):void { + startSlide(event: any): void { if (event.toState === 'collapsed') { this.closed = true; } } - ngOnInit():void { + ngOnInit(): void { this.closed = !this.expanded; this.initializeFilter(); this.collapsed$ = this.isCollapsed(); @@ -82,7 +82,7 @@ export class SidebarFilterComponent implements OnInit { * Checks if the filter is currently collapsed * @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded */ - private isCollapsed():Observable { + private isCollapsed(): Observable { return this.filterService.isCollapsed(this.name); } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts index d25737eaa9..672a7a2a2d 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts @@ -8,17 +8,17 @@ import { * Interface that represents the state for a single filters */ export interface SidebarFilterState { - filterCollapsed:boolean, + filterCollapsed: boolean, } /** * Interface that represents the state for all available filters */ export interface SidebarFiltersState { - [name:string]:SidebarFilterState + [name: string]: SidebarFilterState } -const initialState:SidebarFiltersState = Object.create(null); +const initialState: SidebarFiltersState = Object.create(null); /** * Performs a filter action on the current state @@ -26,7 +26,7 @@ const initialState:SidebarFiltersState = Object.create(null); * @param {SidebarFilterAction} action The action that should be performed * @returns {SidebarFiltersState} The state after the action is performed */ -export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState { +export function sidebarFilterReducer(state = initialState, action: SidebarFilterAction): SidebarFiltersState { switch (action.type) { diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts index 2ff28fd2f5..36244b087c 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.service.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.service.ts @@ -16,7 +16,7 @@ import { hasValue } from '../../empty.util'; @Injectable() export class SidebarFilterService { - constructor(private store:Store) { + constructor(private store: Store) { } /** @@ -24,7 +24,7 @@ export class SidebarFilterService { * @param {string} filter The filter for which the action is dispatched * @param {boolean} expanded If the filter should be open from the start */ - public initializeFilter(filter:string, expanded:boolean):void { + public initializeFilter(filter: string, expanded: boolean): void { this.store.dispatch(new FilterInitializeAction(filter, expanded)); } @@ -32,7 +32,7 @@ export class SidebarFilterService { * Dispatches a collapse action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public collapse(filterName:string):void { + public collapse(filterName: string): void { this.store.dispatch(new FilterCollapseAction(filterName)); } @@ -40,7 +40,7 @@ export class SidebarFilterService { * Dispatches an expand action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public expand(filterName:string):void { + public expand(filterName: string): void { this.store.dispatch(new FilterExpandAction(filterName)); } @@ -48,7 +48,7 @@ export class SidebarFilterService { * Dispatches a toggle action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public toggle(filterName:string):void { + public toggle(filterName: string): void { this.store.dispatch(new FilterToggleAction(filterName)); } @@ -57,10 +57,10 @@ export class SidebarFilterService { * @param {string} filterName The filtername for which the collapsed state is checked * @returns {Observable} Emits the current collapsed state of the given filter, if it's unavailable, return false */ - isCollapsed(filterName:string):Observable { + isCollapsed(filterName: string): Observable { return this.store.pipe( select(filterByNameSelector(filterName)), - map((object:SidebarFilterState) => { + map((object: SidebarFilterState) => { if (object) { return object.filterCollapsed; } else { @@ -73,14 +73,14 @@ export class SidebarFilterService { } -const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter; +const filterStateSelector = (state: SidebarFiltersState) => state.sidebarFilter; -function filterByNameSelector(name:string):MemoizedSelector { +function filterByNameSelector(name: string): MemoizedSelector { return keySelector(name); } -export function keySelector(key:string):MemoizedSelector { - return createSelector(filterStateSelector, (state:SidebarFilterState) => { +export function keySelector(key: string): MemoizedSelector { + return createSelector(filterStateSelector, (state: SidebarFilterState) => { if (hasValue(state)) { return state[key]; } else { diff --git a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts index 77f59090ab..e9211797a9 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts +++ b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts @@ -7,8 +7,8 @@ import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('PageWithSidebarComponent', () => { - let comp:PageWithSidebarComponent; - let fixture:ComponentFixture; + let comp: PageWithSidebarComponent; + let fixture: ComponentFixture; const sidebarService = { isCollapsed: observableOf(true), @@ -42,7 +42,7 @@ describe('PageWithSidebarComponent', () => { }); describe('when sidebarCollapsed is true in mobile view', () => { - let menu:HTMLElement; + let menu: HTMLElement; beforeEach(() => { menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; @@ -58,7 +58,7 @@ describe('PageWithSidebarComponent', () => { }); describe('when sidebarCollapsed is false in mobile view', () => { - let menu:HTMLElement; + let menu: HTMLElement; beforeEach(() => { menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; @@ -70,6 +70,5 @@ describe('PageWithSidebarComponent', () => { it('should open the menu', () => { expect(menu.classList).toContain('active'); }); - }); }); diff --git a/src/app/shared/sidebar/page-with-sidebar.component.ts b/src/app/shared/sidebar/page-with-sidebar.component.ts index 8b7f987a37..44fa238d3b 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.ts +++ b/src/app/shared/sidebar/page-with-sidebar.component.ts @@ -18,13 +18,13 @@ import { map } from 'rxjs/operators'; * the template outlet (inside the page-width-sidebar tags). */ export class PageWithSidebarComponent implements OnInit { - @Input() id:string; - @Input() sidebarContent:TemplateRef; + @Input() id: string; + @Input() sidebarContent: TemplateRef; /** * Emits true if were on a small screen */ - isXsOrSm$:Observable; + isXsOrSm$: Observable; /** * The width of the sidebar (bootstrap columns) @@ -35,16 +35,16 @@ export class PageWithSidebarComponent implements OnInit { /** * Observable for whether or not the sidebar is currently collapsed */ - isSidebarCollapsed$:Observable; + isSidebarCollapsed$: Observable; - sidebarClasses:Observable; + sidebarClasses: Observable; - constructor(protected sidebarService:SidebarService, - protected windowService:HostWindowService, + constructor(protected sidebarService: SidebarService, + protected windowService: HostWindowService, ) { } - ngOnInit():void { + ngOnInit(): void { this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isSidebarCollapsed$ = this.isSidebarCollapsed(); this.sidebarClasses = this.isSidebarCollapsed$.pipe( @@ -56,21 +56,21 @@ export class PageWithSidebarComponent implements OnInit { * Check if the sidebar is collapsed * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded */ - private isSidebarCollapsed():Observable { + private isSidebarCollapsed(): Observable { return this.sidebarService.isCollapsed; } /** * Set the sidebar to a collapsed state */ - public closeSidebar():void { + public closeSidebar(): void { this.sidebarService.collapse() } /** * Set the sidebar to an expanded state */ - public openSidebar():void { + public openSidebar(): void { this.sidebarService.expand(); } diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.ts b/src/app/shared/sidebar/sidebar-dropdown.component.ts index 313538eded..471d357e25 100644 --- a/src/app/shared/sidebar/sidebar-dropdown.component.ts +++ b/src/app/shared/sidebar/sidebar-dropdown.component.ts @@ -10,7 +10,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; * The options should still be provided in the content. */ export class SidebarDropdownComponent { - @Input() id:string; - @Input() label:string; - @Output() change:EventEmitter = new EventEmitter(); + @Input() id: string; + @Input() label: string; + @Output() change: EventEmitter = new EventEmitter(); } diff --git a/src/app/shared/utils/object-keys-pipe.ts b/src/app/shared/utils/object-keys-pipe.ts index fd3d018b88..1320dbc4bf 100644 --- a/src/app/shared/utils/object-keys-pipe.ts +++ b/src/app/shared/utils/object-keys-pipe.ts @@ -10,7 +10,7 @@ export class ObjectKeysPipe implements PipeTransform { * @param value An object * @returns {any} Array with all keys the input object */ - transform(value, args:string[]): any { + transform(value, args: string[]): any { const keys = []; Object.keys(value).forEach((k) => keys.push(k)); return keys; diff --git a/src/app/shared/utils/object-values-pipe.ts b/src/app/shared/utils/object-values-pipe.ts index 79efd1cb76..bb511b4e5c 100644 --- a/src/app/shared/utils/object-values-pipe.ts +++ b/src/app/shared/utils/object-values-pipe.ts @@ -10,7 +10,7 @@ export class ObjectValuesPipe implements PipeTransform { * @param value An object * @returns {any} Array with all values of the input object */ - transform(value, args:string[]): any { + transform(value, args: string[]): any { const values = []; Object.values(value).forEach((v) => values.push(v)); return values; diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts index d89d2d9fc6..39b90af998 100644 --- a/src/app/statistics/angulartics/dspace-provider.spec.ts +++ b/src/app/statistics/angulartics/dspace-provider.spec.ts @@ -5,9 +5,9 @@ import { filter } from 'rxjs/operators'; import { of as observableOf } from 'rxjs'; describe('Angulartics2DSpace', () => { - let provider:Angulartics2DSpace; - let angulartics2:Angulartics2; - let statisticsService:jasmine.SpyObj; + let provider: Angulartics2DSpace; + let angulartics2: Angulartics2; + let statisticsService: jasmine.SpyObj; beforeEach(() => { angulartics2 = { diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index 9ab01f6023..cd1aab94bd 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -9,15 +9,15 @@ import { StatisticsService } from '../statistics.service'; export class Angulartics2DSpace { constructor( - private angulartics2:Angulartics2, - private statisticsService:StatisticsService, + private angulartics2: Angulartics2, + private statisticsService: StatisticsService, ) { } /** * Activates this plugin */ - startTracking():void { + startTracking(): void { this.angulartics2.eventTrack .pipe(this.angulartics2.filterDeveloperMode()) .subscribe((event) => this.eventTrack(event)); diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts index 1151287ea8..85588aeb97 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -11,14 +11,14 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; templateUrl: './view-tracker.component.html', }) export class ViewTrackerComponent implements OnInit { - @Input() object:DSpaceObject; + @Input() object: DSpaceObject; constructor( - public angulartics2:Angulartics2 + public angulartics2: Angulartics2 ) { } - ngOnInit():void { + ngOnInit(): void { this.angulartics2.eventTrack.next({ action: 'pageView', properties: {object: this.object}, diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts index a67ff7613c..58ac1f07ab 100644 --- a/src/app/statistics/statistics.module.ts +++ b/src/app/statistics/statistics.module.ts @@ -25,7 +25,7 @@ import { StatisticsService } from './statistics.service'; * This module handles the statistics */ export class StatisticsModule { - static forRoot():ModuleWithProviders { + static forRoot(): ModuleWithProviders { return { ngModule: StatisticsModule, providers: [ diff --git a/src/app/statistics/statistics.service.spec.ts b/src/app/statistics/statistics.service.spec.ts index 3a416968f8..1d659aac2b 100644 --- a/src/app/statistics/statistics.service.spec.ts +++ b/src/app/statistics/statistics.service.spec.ts @@ -3,16 +3,15 @@ import { RequestService } from '../core/data/request.service'; import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub'; import { getMockRequestService } from '../shared/mocks/mock-request.service'; import { TrackRequest } from './track-request.model'; -import { ResourceType } from '../core/shared/resource-type'; -import { SearchOptions } from '../+search-page/search-options.model'; import { isEqual } from 'lodash'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; +import { SearchOptions } from '../shared/search/search-options.model'; describe('StatisticsService', () => { - let service:StatisticsService; - let requestService:jasmine.SpyObj; + let service: StatisticsService; + let requestService: jasmine.SpyObj; const restURL = 'https://rest.api'; - const halService:any = new HALEndpointServiceStub(restURL); + const halService: any = new HALEndpointServiceStub(restURL); function initTestService() { return new StatisticsService( @@ -26,9 +25,9 @@ describe('StatisticsService', () => { service = initTestService(); it('should send a request to track an item view ', () => { - const mockItem:any = {uuid: 'mock-item-uuid', type: 'item'}; + const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'}; service.trackViewEvent(mockItem); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; expect(request.body).toBeDefined('request.body'); const body = JSON.parse(request.body); expect(body.targetId).toBe('mock-item-uuid'); @@ -40,7 +39,7 @@ describe('StatisticsService', () => { requestService = getMockRequestService(); service = initTestService(); - const mockSearch:any = new SearchOptions({ + const mockSearch: any = new SearchOptions({ query: 'mock-query', }); @@ -52,7 +51,7 @@ describe('StatisticsService', () => { }; const sort = {by: 'search-field', order: 'ASC'}; service.trackSearchEvent(mockSearch, page, sort); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; const body = JSON.parse(request.body); it('should specify the right query', () => { @@ -80,7 +79,7 @@ describe('StatisticsService', () => { requestService = getMockRequestService(); service = initTestService(); - const mockSearch:any = new SearchOptions({ + const mockSearch: any = new SearchOptions({ query: 'mock-query', configuration: 'mock-configuration', dsoType: DSpaceObjectType.ITEM, @@ -109,7 +108,7 @@ describe('StatisticsService', () => { } ]; service.trackSearchEvent(mockSearch, page, sort, filters); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; const body = JSON.parse(request.body); it('should specify the dsoType', () => { diff --git a/src/app/statistics/statistics.service.ts b/src/app/statistics/statistics.service.ts index cd89125b3c..0c16dc4755 100644 --- a/src/app/statistics/statistics.service.ts +++ b/src/app/statistics/statistics.service.ts @@ -3,10 +3,10 @@ import { Injectable } from '@angular/core'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { map, take } from 'rxjs/operators'; import { TrackRequest } from './track-request.model'; -import { SearchOptions } from '../+search-page/search-options.model'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { HALEndpointService } from '../core/shared/hal-endpoint.service'; import { RestRequest } from '../core/data/request.models'; +import { SearchOptions } from '../shared/search/search-options.model'; /** * The statistics service @@ -15,24 +15,24 @@ import { RestRequest } from '../core/data/request.models'; export class StatisticsService { constructor( - protected requestService:RequestService, - protected halService:HALEndpointService, + protected requestService: RequestService, + protected halService: HALEndpointService, ) { } - private sendEvent(linkPath:string, body:any) { + private sendEvent(linkPath: string, body: any) { const requestId = this.requestService.generateRequestId(); this.halService.getEndpoint(linkPath).pipe( - map((endpoint:string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))), + map((endpoint: string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))), take(1) // otherwise the previous events will fire again - ).subscribe((request:RestRequest) => this.requestService.configure(request)); + ).subscribe((request: RestRequest) => this.requestService.configure(request)); } /** * To track a page view * @param dso: The dso which was viewed */ - trackViewEvent(dso:DSpaceObject) { + trackViewEvent(dso: DSpaceObject) { this.sendEvent('/statistics/viewevents', { targetId: dso.uuid, targetType: (dso as any).type @@ -47,10 +47,10 @@ export class StatisticsService { * @param filters: An array of search filters used to filter the result set */ trackSearchEvent( - searchOptions:SearchOptions, - page:{ size:number, totalElements:number, totalPages:number, number:number }, - sort:{ by:string, order:string }, - filters?:Array<{ filter:string, operator:string, value:string, label:string }> + searchOptions: SearchOptions, + page: { size: number, totalElements: number, totalPages: number, number: number }, + sort: { by: string, order: string }, + filters?: Array<{ filter: string, operator: string, value: string, label: string }> ) { const body = { query: searchOptions.query, diff --git a/tslint.json b/tslint.json index b4f905d324..dd04bffb04 100644 --- a/tslint.json +++ b/tslint.json @@ -114,6 +114,13 @@ "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" } ], "unified-signatures": true, From ef1ed04fd2579ad7815f5ac4d83abf22f9bd6578 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 3 Dec 2019 13:57:56 +0100 Subject: [PATCH 30/77] manual change detection for preview list in submission --- ...-dynamic-form-control-container.component.html | 3 ++- ...ds-dynamic-form-control-container.component.ts | 15 +++++++++------ .../existing-metadata-list-element.component.ts | 1 - 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index d2a1996a60..a31171d7ef 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -54,7 +54,8 @@
        - , - private submissionObjectService: SubmissionObjectDataService + private submissionObjectService: SubmissionObjectDataService, + private ref: ChangeDetectorRef ) { super(componentFactoryResolver, layoutService, validationService); } @@ -228,6 +229,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.hasRelationLookup = hasValue(this.model.relationship); this.reorderables = []; if (this.hasRelationLookup) { + this.listId = 'list-' + this.model.relationship.relationshipType; const item$ = this.submissionObjectService .findById(this.model.submissionId).pipe( @@ -271,7 +273,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo ); this.subs.push(this.reorderables$.subscribe((rs) => { - this.reorderables = rs + this.reorderables = rs; + this.ref.detectChanges(); })); this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( @@ -335,8 +338,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } moveSelection(event: CdkDragDrop) { - moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); this.zone.runOutsideAngular(() => { + moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); const relationships = this.reorderables.map((reo: Reorderable, index: number) => { reo.oldIndex = reo.getPlace(); reo.newIndex = index; @@ -351,7 +354,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return observableOf(undefined); } }) - ).subscribe(); + ).pipe(getSucceededRemoteData()).subscribe(); }) } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index def879701e..c222feb3f0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -85,7 +85,6 @@ export class ExistingMetadataListElementComponent implements OnInit, OnChanges, filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) ).subscribe((item: Item) => { this.relatedItem = item; - const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); if (hasValue(relationMD)) { const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); From 71a3a22a7c8cfa75bcf1759709fd4aa7d7ea5f33 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 3 Dec 2019 14:46:33 +0100 Subject: [PATCH 31/77] fix infinite loop --- src/app/core/data/relationship.service.ts | 117 +++++++++++------- ...ynamic-form-control-container.component.ts | 6 +- 2 files changed, 78 insertions(+), 45 deletions(-) diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index fb9e75bd4d..21e5cd8794 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,34 +1,61 @@ -import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, filter, map, mergeMap, skipWhile, startWith, switchMap, take, tap } from 'rxjs/operators'; -import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; -import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models'; -import { Observable } from 'rxjs/internal/Observable'; -import { RestResponse } from '../cache/response.models'; -import { Item } from '../shared/item.model'; -import { Relationship } from '../shared/item-relationships/relationship.model'; -import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { RemoteData } from './remote-data'; -import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; -import { PaginatedList } from './paginated-list'; -import { ItemDataService } from './item-data.service'; -import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DataService } from './data.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Injectable } from '@angular/core'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; +import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { + distinctUntilChanged, + filter, + map, + mergeMap, + startWith, + switchMap, + take, + tap +} from 'rxjs/operators'; +import { + compareArraysUsingIds, + paginatedRelationsToItems, + relationsToItems +} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { AppState, keySelector } from '../../app.reducer'; +import { + hasValue, + hasValueOperator, + isNotEmpty, + isNotEmptyOperator +} from '../../shared/empty.util'; +import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; +import { + RemoveNameVariantAction, + SetNameVariantAction +} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; -import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { Relationship } from '../shared/item-relationships/relationship.model'; +import { Item } from '../shared/item.model'; +import { + configureRequest, + getRemoteDataPayload, + getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { ItemDataService } from './item-data.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData, RemoteDataState } from './remote-data'; +import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models'; +import { RequestService } from './request.service'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -116,9 +143,9 @@ export class RelationshipService extends DataService { this.findById(relationshipId).pipe( getSucceededRemoteData(), getRemoteDataPayload(), - switchMap((relationship: Relationship) => combineLatest( - relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), - relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) + switchMap((rel: Relationship) => combineLatest( + rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), + rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) ) ), take(1) @@ -336,20 +363,26 @@ export class RelationshipService extends DataService { ) } - public updatePlace(relationship: Relationship, newIndex: number, left: boolean): Observable> { + public updatePlace(reoRel: ReorderableRelationship): Observable> { let updatedRelationship; - if (left) { - updatedRelationship = Object.assign(new Relationship(), relationship, { leftPlace: newIndex }); + if (reoRel.useLeftItem) { + updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { rightPlace: reoRel.newIndex }); } else { - updatedRelationship = Object.assign(new Relationship(), relationship, { rightPlace: newIndex }); + updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { leftPlace: reoRel.newIndex }); } - return this.update(updatedRelationship).pipe( - tap((relationshipRD: RemoteData) => { - if (relationshipRD.hasSucceeded) { - this.removeRelationshipItemsFromCacheByRelationship(relationship.id); - } - }) - ); + + const update$ = this.update(updatedRelationship); + + update$.pipe( + filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.ResponsePending), + take(1), + ).subscribe((relationshipRD: RemoteData) => { + if (relationshipRD.state === RemoteDataState.ResponsePending) { + this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id); + } + }); + + return update$; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index ce3a09689c..82f65c940f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -340,16 +340,16 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo moveSelection(event: CdkDragDrop) { this.zone.runOutsideAngular(() => { moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); - const relationships = this.reorderables.map((reo: Reorderable, index: number) => { + const reorderables = this.reorderables.map((reo: Reorderable, index: number) => { reo.oldIndex = reo.getPlace(); reo.newIndex = index; return reo; } ); - return observableCombineLatest(relationships.map((rel: ReorderableRelationship) => { + return observableCombineLatest(reorderables.map((rel: ReorderableRelationship) => { console.log(rel); if (rel.oldIndex !== rel.newIndex) { - return this.relationshipService.updatePlace(rel.relationship, rel.newIndex, !rel.useLeftItem); + return this.relationshipService.updatePlace(rel); } else { return observableOf(undefined); } From b3e2041cdb63e46ab559540d230fb7c1aaf16e12 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 3 Dec 2019 14:47:47 +0100 Subject: [PATCH 32/77] 67478: External source tabs test cases --- .../core/data/external-source.service.spec.ts | 76 ++++++++ .../core/data/lookup-relation.service.spec.ts | 110 ++++++++++++ ...-list-submission-element.component.spec.ts | 47 +++++ ...ic-lookup-relation-modal.component.spec.ts | 39 +++++ ...elation-external-source-tab.component.html | 4 +- ...tion-external-source-tab.component.spec.ts | 162 ++++++++++++++++++ ...okup-relation-search-tab.component.spec.ts | 9 + 7 files changed, 445 insertions(+), 2 deletions(-) create mode 100644 src/app/core/data/external-source.service.spec.ts create mode 100644 src/app/core/data/lookup-relation.service.spec.ts create mode 100644 src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts new file mode 100644 index 0000000000..77a2a85dfd --- /dev/null +++ b/src/app/core/data/external-source.service.spec.ts @@ -0,0 +1,76 @@ +import { ExternalSourceService } from './external-source.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { of as observableOf } from 'rxjs'; +import { GetRequest } from './request.models'; + +describe('ExternalSourceService', () => { + let service: ExternalSourceService; + + let requestService; + let rdbService; + let halService; + + const entries = [ + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }) + ]; + + function init() { + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: 'request-uuid', + configure: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + }); + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf('external-sources-REST-endpoint') + }); + service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined); + } + + beforeEach(() => { + init(); + }); + + describe('getExternalSourceEntries', () => { + let result; + + beforeEach(() => { + result = service.getExternalSourceEntries('test'); + }); + + it('should configure a GetRequest', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); + }); + }); +}); diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts new file mode 100644 index 0000000000..5ac29b5030 --- /dev/null +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -0,0 +1,110 @@ +import { LookupRelationService } from './lookup-relation.service'; +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { Item } from '../shared/item.model'; +import { skip, take } from 'rxjs/operators'; + +describe('LookupRelationService', () => { + let service: LookupRelationService; + let externalSourceService: ExternalSourceService; + let searchService: SearchService; + + const totalExternal = 8; + const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); + const relationship = Object.assign(new RelationshipOptions(), { + filter: 'test-filter', + configuration: 'test-configuration' + }); + const localResults = [ + Object.assign(new SearchResult(), { + indexableObject: Object.assign(new Item(), { + uuid: 'test-item-uuid', + handle: 'test-item-handle' + }) + }) + ]; + + function init() { + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) + }); + searchService = jasmine.createSpyObj('searchService', { + search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)) + }); + service = new LookupRelationService(externalSourceService, searchService); + } + + beforeEach(() => { + init(); + }); + + describe('getLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getLocalResults(relationship, optionsWithQuery); + }); + + it('should return the local results', () => { + result.subscribe((resultsRD) => { + expect(resultsRD.payload.page).toBe(localResults); + }); + }); + + it('should set the searchConfig to contain a fixedFilter and configuration', () => { + expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + )); + }); + }); + + describe('getTotalLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalLocalResults(relationship, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(localResults.length) + }); + }); + + it('should not set searchConfig', () => { + expect(service.searchConfig).toBeUndefined(); + }); + }); + + describe('getTotalExternalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalExternalResults(relationship, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(totalExternal) + }); + }); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..fa153b8c5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('ExternalSourceEntryListSubmissionElementComponent', () => { + let component: ExternalSourceEntryListSubmissionElementComponent; + let fixture: ComponentFixture; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryListSubmissionElementComponent], + imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent); + component = fixture.componentInstance; + component.object = entry; + fixture.detectChanges(); + }); + + it('should display the entry\'s display value', () => { + expect(fixture.nativeElement.textContent).toContain(entry.display); + }); + + it('should display the entry\'s uri', () => { + expect(fixture.nativeElement.textContent).toContain(uri); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts index a4f77fd364..d1b289bf11 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -13,6 +13,12 @@ import { Item } from '../../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions'; +import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; +import { PaginatedSearchOptions } from '../../../../search/paginated-search-options.model'; +import { ExternalSource } from '../../../../../core/shared/external-source.model'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { ExternalSourceService } from '../../../../../core/data/external-source.service'; +import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; describe('DsDynamicLookupRelationModalComponent', () => { let component: DsDynamicLookupRelationModalComponent; @@ -28,6 +34,24 @@ describe('DsDynamicLookupRelationModalComponent', () => { let relationship; let nameVariant; let metadataField; + let pSearchOptions; + let externalSourceService; + let lookupRelationService; + + const externalSources = [ + Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }), + Object.assign(new ExternalSource(), { + id: 'sherpaPublisher', + name: 'sherpaPublisher', + hierarchical: false + }) + ]; + const totalLocal = 10; + const totalExternal = 8; function init() { item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} }); @@ -41,6 +65,14 @@ describe('DsDynamicLookupRelationModalComponent', () => { relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; nameVariant = 'Doe, J.'; metadataField = 'dc.contributor.author'; + pSearchOptions = new PaginatedSearchOptions({}); + externalSourceService = jasmine.createSpyObj('externalSourceService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources)) + }); + lookupRelationService = jasmine.createSpyObj('lookupRelationService', { + getTotalLocalResults: observableOf(totalLocal), + getTotalExternalResults: observableOf(totalExternal) + }); } beforeEach(async(() => { @@ -49,6 +81,13 @@ describe('DsDynamicLookupRelationModalComponent', () => { declarations: [DsDynamicLookupRelationModalComponent], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()], providers: [ + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + } + }, + { provide: ExternalSourceService, useValue: externalSourceService }, + { provide: LookupRelationService, useValue: lookupRelationService }, { provide: SelectableListService, useValue: selectableListService }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html index 6b66be97c4..9536d0a5cb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -8,7 +8,7 @@

        {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

        - -
        +
        {{ 'search.results.empty' | translate }}
        diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts new file mode 100644 index 0000000000..62327e236e --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -0,0 +1,162 @@ +import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../../../../../utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { + createFailedRemoteDataObject$, + createPaginatedList, + createPendingRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../../../../testing/utils'; +import { ExternalSourceService } from '../../../../../../core/data/external-source.service'; +import { ExternalSource } from '../../../../../../core/shared/external-source.model'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; +import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; + +describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { + let component: DsDynamicLookupRelationExternalSourceTabComponent; + let fixture: ComponentFixture; + let pSearchOptions; + let externalSourceService; + + const externalSource = { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + } as ExternalSource; + const externalEntries = [ + Object.assign({ + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign({ + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }), + Object.assign({ + id: '0001-0001-0001-0003', + display: 'Edwards Anna', + value: 'Edwards, Anna', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0003' + } + ] + } + }) + ] as ExternalSourceEntry[]; + + function init() { + pSearchOptions = new PaginatedSearchOptions({ + query: 'test' + }); + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(createPaginatedList(externalEntries)) + }); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicLookupRelationExternalSourceTabComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), BrowserAnimationsModule], + providers: [ + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + } + }, + { provide: ExternalSourceService, useValue: externalSourceService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicLookupRelationExternalSourceTabComponent); + component = fixture.componentInstance; + component.externalSource = externalSource; + fixture.detectChanges(); + }); + + describe('when the external entries finished loading successfully', () => { + it('should display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeDefined(); + }); + }); + + describe('when the external entries are loading', () => { + beforeEach(() => { + component.entriesRD$ = createPendingRemoteDataObject$(undefined); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a ds-loading component', () => { + const loading = fixture.debugElement.query(By.css('ds-loading')); + expect(loading).not.toBeNull(); + }); + }); + + describe('when the external entries failed loading', () => { + beforeEach(() => { + component.entriesRD$ = createFailedRemoteDataObject$(undefined); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a ds-error component', () => { + const error = fixture.debugElement.query(By.css('ds-error')); + expect(error).not.toBeNull(); + }); + }); + + describe('when the external entries return an empty list', () => { + beforeEach(() => { + component.entriesRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([])); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a message the list is empty', () => { + const empty = fixture.debugElement.query(By.css('#empty-external-entry-list')); + expect(empty).not.toBeNull(); + }); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts index 241d022dc3..3d713b15fe 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts @@ -15,6 +15,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils' import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { Item } from '../../../../../../core/shared/item.model'; +import { ActivatedRoute } from '@angular/router'; +import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; describe('DsDynamicLookupRelationSearchTabComponent', () => { let component: DsDynamicLookupRelationSearchTabComponent; @@ -32,6 +34,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { let results; let selectableListService; + let lookupRelationService; function init() { relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; @@ -47,6 +50,10 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']); + lookupRelationService = jasmine.createSpyObj('lookupRelationService', { + getLocalResults: createSuccessfulRemoteDataObject$(results) + }); + lookupRelationService.searchConfig = {}; } beforeEach(async(() => { @@ -71,6 +78,8 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { } } }, + { provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } }, + { provide: LookupRelationService, useValue: lookupRelationService } ], schemas: [NO_ERRORS_SCHEMA] }) From 558285da85712d3a483de6ba4c88f6d50fccfcb7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 3 Dec 2019 14:56:05 +0100 Subject: [PATCH 33/77] 67478: Automatically fill query in search form --- .../dynamic-lookup-relation-search-tab.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index f5b9a86758..36197b33c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -3,7 +3,7 @@ [resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="true" [showViewModes]="false">
        - + From 5234a7b01566a09994927c750f0f31a3250a7008 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 3 Dec 2019 15:09:29 +0100 Subject: [PATCH 34/77] fix loop for name variants as well --- src/app/core/data/data.service.ts | 1 + src/app/core/data/relationship.service.ts | 20 +++++++++++-------- .../relationship.effects.ts | 3 ++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index b8e511524d..189ffef6b8 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -236,6 +236,7 @@ export abstract class DataService { console.log(oldVersion); const operations = this.comparator.diff(oldVersion, object); if (isNotEmpty(operations)) { + console.log('operations', operations); this.objectCache.addPatch(object.self, operations); } return this.findByHref(object.self); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 21e5cd8794..d8cc027348 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -333,7 +333,7 @@ export class RelationshipService extends DataService { } public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { - return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) + const update$ = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( switchMap((relation: Relationship) => relation.relationshipType.pipe( @@ -354,13 +354,17 @@ export class RelationshipService extends DataService { } return this.update(updatedRelationship); }), - tap((relationshipRD: RemoteData) => { - if (relationshipRD.hasSucceeded) { - this.removeRelationshipItemsFromCache(item1); - this.removeRelationshipItemsFromCache(item2); - } - }), - ) + ); + + update$.pipe( + filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.ResponsePending), + take(1), + ).subscribe(() => { + this.removeRelationshipItemsFromCache(item1); + this.removeRelationshipItemsFromCache(item2); + }); + + return update$ } public updatePlace(reoRel: ReorderableRelationship): Observable> { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 7b530f39c5..a17e042c2b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { getSucceededRemoteData } from '../../../../../core/shared/operators'; import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { Item } from '../../../../../core/shared/item.model'; import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util'; @@ -83,7 +84,7 @@ export class RelationshipEffects { this.nameVariantUpdates[identifier] = nameVariant; } else { this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant) - .pipe() + .pipe(getSucceededRemoteData()) .subscribe(); } } From e9a8a25116943e54d4cf32bc9e8e0e4bb46697f8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 3 Dec 2019 15:40:10 +0100 Subject: [PATCH 35/77] 67478: AoT build fixes --- src/app/core/data/lookup-relation.service.spec.ts | 8 +++++++- ...namic-lookup-relation-external-source-tab.component.ts | 2 +- .../dynamic-lookup-relation-search-tab.component.ts | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index 5ac29b5030..321fd8d218 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -9,6 +9,7 @@ import { RelationshipOptions } from '../../shared/form/builder/models/relationsh import { SearchResult } from '../../shared/search/search-result.model'; import { Item } from '../shared/item.model'; import { skip, take } from 'rxjs/operators'; +import { ExternalSource } from '../shared/external-source.model'; describe('LookupRelationService', () => { let service: LookupRelationService; @@ -29,6 +30,11 @@ describe('LookupRelationService', () => { }) }) ]; + const externalSource = Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }); function init() { externalSourceService = jasmine.createSpyObj('externalSourceService', { @@ -92,7 +98,7 @@ describe('LookupRelationService', () => { let result; beforeEach(() => { - result = service.getTotalExternalResults(relationship, optionsWithQuery); + result = service.getTotalExternalResults(externalSource, optionsWithQuery); }); it('should start with 0', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index aae7b8b2f1..efee24b8ef 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -55,7 +55,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit entriesRD$: Observable>>; constructor(private router: Router, - private searchConfigService: SearchConfigurationService, + public searchConfigService: SearchConfigurationService, private externalSourceService: ExternalSourceService) { } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 40d64ca939..fb2884cb37 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -59,9 +59,9 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest private router: Router, private route: ActivatedRoute, private selectableListService: SelectableListService, - private searchConfigService: SearchConfigurationService, + public searchConfigService: SearchConfigurationService, private routeService: RouteService, - protected lookupRelationService: LookupRelationService + public lookupRelationService: LookupRelationService ) { } From 69d58c08819d038c04ed1bdb9861ecf3a7597885 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 3 Dec 2019 16:37:58 +0100 Subject: [PATCH 36/77] removed console logs and reinstated thumbnails --- src/app/core/data/data.service.ts | 2 -- src/app/core/data/relationship.service.ts | 2 +- .../person/person-item-metadata-list-element.component.ts | 5 +---- ...nit-search-result-list-submission-element.component.html | 2 +- ...son-search-result-list-submission-element.component.html | 2 +- .../ds-dynamic-form-control-container.component.ts | 1 - .../existing-metadata-list-element.component.ts | 6 +----- 7 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 189ffef6b8..ddf2c3a1d5 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -233,10 +233,8 @@ export abstract class DataService { getSucceededRemoteData(), getRemoteDataPayload(), mergeMap((oldVersion: T) => { - console.log(oldVersion); const operations = this.comparator.diff(oldVersion, object); if (isNotEmpty(operations)) { - console.log('operations', operations); this.objectCache.addPatch(object.self, operations); } return this.findByHref(object.self); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index d8cc027348..9bd59ce151 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -357,7 +357,7 @@ export class RelationshipService extends DataService { ); update$.pipe( - filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.ResponsePending), + filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.RequestPending), take(1), ).subscribe(() => { this.removeRelationshipItemsFromCache(item1); diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts index b571348122..867b5890eb 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts @@ -11,8 +11,5 @@ import { ItemMetadataRepresentationListElementComponent } from '../../../../shar /** * The component for displaying an item of the type Person as a metadata field */ -export class PersonItemMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent implements OnInit { - ngOnInit(): void { - console.log('this.metadataRepresentation', this.metadataRepresentation); - } +export class PersonItemMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent { } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index 61bcbc9a4f..b0fa714371 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -1,6 +1,6 @@
        - +
        - +
        diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 82f65c940f..f1b8bd40b5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -347,7 +347,6 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } ); return observableCombineLatest(reorderables.map((rel: ReorderableRelationship) => { - console.log(rel); if (rel.oldIndex !== rel.newIndex) { return this.relationshipService.updatePlace(rel); } else { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index c222feb3f0..a0de289a04 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -51,7 +51,7 @@ export class ReorderableRelationship extends Reorderable { templateUrl: './existing-metadata-list-element.component.html', styleUrls: ['./existing-metadata-list-element.component.scss'] }) -export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy { +export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy { @Input() listId: string; @Input() submissionItem: Item; @Input() reoRel: ReorderableRelationship; @@ -71,10 +71,6 @@ export class ExistingMetadataListElementComponent implements OnInit, OnChanges, ) { } - ngOnInit(): void { - console.log('reoRel', this.reoRel); - } - ngOnChanges() { const item$ = this.reoRel.useLeftItem ? this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; From 0210be1a53faeaef1fb93d886b29db94ebe682be Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 4 Dec 2019 17:54:01 +0100 Subject: [PATCH 37/77] 67611: External-Source-Entry import window --- resources/i18n/en.json5 | 26 +++ .../normalized-external-source-entry.model.ts | 6 + .../shared/external-source-entry.model.ts | 5 + ...try-list-submission-element.component.html | 13 +- ...entry-list-submission-element.component.ts | 28 ++- ...namic-lookup-relation-modal.component.html | 1 + ...namic-lookup-relation-modal.component.scss | 8 + ...elation-external-source-tab.component.html | 1 - ...-relation-external-source-tab.component.ts | 2 + ...l-source-entry-import-modal.component.html | 50 ++++++ ...l-source-entry-import-modal.component.scss | 3 + ...ource-entry-import-modal.component.spec.ts | 0 ...nal-source-entry-import-modal.component.ts | 162 ++++++++++++++++++ src/app/shared/shared.module.ts | 7 +- 14 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index df0d9b27f0..fe7949d8b1 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1525,6 +1525,32 @@ "submission.sections.describe.relationship-lookup.close": "Close", + "submission.sections.describe.relationship-lookup.external-source.import-button-title": "Import remote entry", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.authority.new": "Import as a new local authority entry", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaPublisher": "Importing from Sherpa Publisher", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.title": "Import Remote Entry", + "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts index 02abbb32d8..d09d1fe7ea 100644 --- a/src/app/core/cache/models/normalized-external-source-entry.model.ts +++ b/src/app/core/cache/models/normalized-external-source-entry.model.ts @@ -25,6 +25,12 @@ export class NormalizedExternalSourceEntry extends NormalizedObject{{object.display}}
        - +
        + +
        +
        +
        {{object.display}}
        + +
        diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index 673370cc2c..07d691a5cb 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -3,9 +3,13 @@ import { ExternalSourceEntry } from '../../../../../core/shared/external-source- import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { Context } from '../../../../../core/shared/context.model'; -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { Metadata } from '../../../../../core/shared/metadata.utils'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ExternalSourceEntryImportModalComponent } from '../../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; +import { DsDynamicLookupRelationExternalSourceTabComponent } from '../../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; +import { hasValue } from '../../../../../shared/empty.util'; @listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) @Component({ @@ -19,7 +23,29 @@ export class ExternalSourceEntryListSubmissionElementComponent extends AbstractL */ uri: MetadataValue; + /** + * The modal for importing the entry + */ + modalRef: NgbModalRef; + + constructor(@Inject(DsDynamicLookupRelationExternalSourceTabComponent) private externalSourceTab: DsDynamicLookupRelationExternalSourceTabComponent, + private modalService: NgbModal) { + super(); + } + ngOnInit(): void { this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); } + + import(): void { + this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, { + size: 'lg', + container: 'ds-dynamic-lookup-relation-modal' + }); + const modalComp = this.modalRef.componentInstance; + modalComp.externalSourceEntry = this.object; + if (hasValue(this.externalSourceTab) && hasValue(this.externalSourceTab.relationship)) { + modalComp.relationship = this.externalSourceTab.relationship; + } + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 46620aa00b..4c87ac8bdb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -26,6 +26,7 @@ = new EventEmitter(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html new file mode 100644 index 0000000000..d2ad3d403d --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -0,0 +1,50 @@ + + + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.scss new file mode 100644 index 0000000000..7db9839e38 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.scss @@ -0,0 +1,3 @@ +.modal-footer { + justify-content: space-between; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts new file mode 100644 index 0000000000..6a65ec9b05 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -0,0 +1,162 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model'; +import { MetadataValue } from '../../../../../../../core/shared/metadata.models'; +import { Metadata } from '../../../../../../../core/shared/metadata.utils'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../../../../core/data/paginated-list'; +import { SearchResult } from '../../../../../../search/search-result.model'; +import { Item } from '../../../../../../../core/shared/item.model'; +import { RelationshipOptions } from '../../../../models/relationship-options.model'; +import { LookupRelationService } from '../../../../../../../core/data/lookup-relation.service'; +import { PaginatedSearchOptions } from '../../../../../../search/paginated-search-options.model'; +import { CollectionElementLinkType } from '../../../../../../object-collection/collection-element-link.type'; +import { Context } from '../../../../../../../core/shared/context.model'; +import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; + +/** + * The possible types of import for the external entry + */ +export enum ImportType { + None = 'None', + LocalEntity = 'LocalEntity', + LocalAuthority = 'LocalAuthority', + NewEntity = 'NewEntity', + NewAuthority = 'NewAuthority' +} + +@Component({ + selector: 'ds-external-source-entry-import-modal', + styleUrls: ['./external-source-entry-import-modal.component.scss'], + templateUrl: './external-source-entry-import-modal.component.html' +}) +export class ExternalSourceEntryImportModalComponent implements OnInit { + /** + * The external source entry + */ + externalSourceEntry: ExternalSourceEntry; + + /** + * The current relationship-options used for filtering results + */ + relationship: RelationshipOptions; + + /** + * The metadata value for the entry's uri + */ + uri: MetadataValue; + + /** + * Local entities with a similar name + */ + localEntitiesRD$: Observable>>>; + + /** + * Search options to use for fetching similar results + */ + searchOptions: PaginatedSearchOptions; + + /** + * The type of link to render in listable elements + */ + linkTypes = CollectionElementLinkType; + + /** + * The context we're currently in (submission) + */ + context = Context.SubmissionModal; + + /** + * List ID for selecting local entities + */ + entityListId = 'external-source-import-entity'; + + /** + * List ID for selecting local authorities + */ + authorityListId = 'external-source-import-authority'; + + /** + * ImportType enum + */ + importType = ImportType; + + /** + * The type of import the user currently has selected + */ + selectedImportType = ImportType.None; + + constructor(public modal: NgbActiveModal, + public lookupRelationService: LookupRelationService, + private selectService: SelectableListService) { + } + + ngOnInit(): void { + this.uri = Metadata.first(this.externalSourceEntry.metadata, 'dc.identifier.uri'); + this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: this.externalSourceEntry.value })); + this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions); + } + + /** + * Close the window + */ + close() { + this.modal.close(); + } + + /** + * Perform the import of the external entry + */ + import() { + console.log('TODO: Import'); + } + + /** + * Deselected a local entity + * @param event + */ + deselectEntity(event) { + this.selectedImportType = ImportType.None; + } + + /** + * Selected a local entity + * @param event + */ + selectEntity(event) { + this.selectedImportType = ImportType.LocalEntity; + } + + /** + * Selected/deselected the new entity option + */ + selectNewEntity() { + if (this.selectedImportType === ImportType.NewEntity) { + this.selectedImportType = ImportType.None; + } else { + this.selectedImportType = ImportType.NewEntity; + this.deselectAllLists(); + } + } + + /** + * Selected/deselected the new authority option + */ + selectNewAuthority() { + if (this.selectedImportType === ImportType.NewAuthority) { + this.selectedImportType = ImportType.None; + } else { + this.selectedImportType = ImportType.NewAuthority; + this.deselectAllLists(); + } + } + + /** + * Deselect every element from both entity and authority lists + */ + deselectAllLists() { + this.selectService.deselectAll(this.entityListId); + this.selectService.deselectAll(this.authorityListId); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 903083e931..230bb1f073 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -175,6 +175,7 @@ import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-f import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; +import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -331,7 +332,8 @@ const COMPONENTS = [ ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, - SelectableListItemControlComponent + SelectableListItemControlComponent, + ExternalSourceEntryImportModalComponent ]; const ENTRY_COMPONENTS = [ @@ -393,7 +395,8 @@ const ENTRY_COMPONENTS = [ SearchAuthorityFilterComponent, DsDynamicLookupRelationSearchTabComponent, DsDynamicLookupRelationSelectionTabComponent, - DsDynamicLookupRelationExternalSourceTabComponent + DsDynamicLookupRelationExternalSourceTabComponent, + ExternalSourceEntryImportModalComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ From 6ada3fae5b5288f5e321ece59bf71fc5ae1ec462 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 5 Dec 2019 10:58:38 +0100 Subject: [PATCH 38/77] 67611: Fixed radio button bug + hide pagintion details --- .../external-source-entry-import-modal.component.html | 5 ++++- .../external-source-entry-import-modal.component.ts | 6 ++++-- .../object-collection/object-collection.component.html | 3 +++ .../shared/object-collection/object-collection.component.ts | 5 +++++ src/app/shared/object-detail/object-detail.component.ts | 2 +- src/app/shared/object-grid/object-grid.component.html | 1 + src/app/shared/object-grid/object-grid.component.ts | 5 +++++ src/app/shared/object-list/object-list.component.html | 1 + src/app/shared/object-list/object-list.component.ts | 5 +++++ src/app/shared/pagination/pagination.component.html | 2 +- .../search/search-results/search-results.component.html | 1 + .../search/search-results/search-results.component.ts | 5 +++++ 12 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html index d2ad3d403d..f055e617e3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -16,10 +16,13 @@
        {{ ('submission.sections.describe.relationship-lookup.external-source.import-modal.entities' | translate) }}
        - diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index f09ba3953e..58de49ba40 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -63,6 +63,11 @@ export class ObjectCollectionComponent implements OnInit { */ @Input() context: Context; + /** + * Option for hiding the pagination detail + */ + @Input() hidePaginationDetail = false; + /** * the page info of the list */ diff --git a/src/app/shared/object-detail/object-detail.component.ts b/src/app/shared/object-detail/object-detail.component.ts index fb68316251..45efb4a0b7 100644 --- a/src/app/shared/object-detail/object-detail.component.ts +++ b/src/app/shared/object-detail/object-detail.component.ts @@ -89,7 +89,7 @@ export class ObjectDetailComponent { /** * Option for hiding the pagination detail */ - public hidePaginationDetail = true; + @Input() hidePaginationDetail = true; /** * An event fired when the page is changed. diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 348536bfed..0afd623d86 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -5,6 +5,7 @@ [sortOptions]="sortConfig" [hideGear]="hideGear" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" + [hidePaginationDetail]="hidePaginationDetail" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" diff --git a/src/app/shared/object-grid/object-grid.component.ts b/src/app/shared/object-grid/object-grid.component.ts index 2da4abe13b..c6f8347217 100644 --- a/src/app/shared/object-grid/object-grid.component.ts +++ b/src/app/shared/object-grid/object-grid.component.ts @@ -69,6 +69,11 @@ export class ObjectGridComponent implements OnInit { */ @Input() context: Context; + /** + * Option for hiding the pagination detail + */ + @Input() hidePaginationDetail = false; + /** * Behavior subject to output the current listable objects */ diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 887be96785..d0cfb1a9d8 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -5,6 +5,7 @@ [sortOptions]="sortConfig" [hideGear]="hideGear" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" + [hidePaginationDetail]="hidePaginationDetail" (pageChange)="onPageChange($event)" (pageSizeChange)="onPageSizeChange($event)" (sortDirectionChange)="onSortDirectionChange($event)" diff --git a/src/app/shared/object-list/object-list.component.ts b/src/app/shared/object-list/object-list.component.ts index 1be0f69106..1efd715084 100644 --- a/src/app/shared/object-list/object-list.component.ts +++ b/src/app/shared/object-list/object-list.component.ts @@ -74,6 +74,11 @@ export class ObjectListComponent { */ @Input() context: Context; + /** + * Option for hiding the pagination detail + */ + @Input() hidePaginationDetail = false; + /** * The current listable objects */ diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index c16a153026..649fe686ff 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,5 +1,5 @@
        -
        +
        {{ 'pagination.showing.label' | translate }} diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index ab1e96c58f..cbc56d1080 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -8,6 +8,7 @@ [selectable]="selectable" [selectionConfig]="selectionConfig" [context]="context" + [hidePaginationDetail]="hidePaginationDetail" (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)" > diff --git a/src/app/shared/search/search-results/search-results.component.ts b/src/app/shared/search/search-results/search-results.component.ts index f245b5f9ae..b094e69a57 100644 --- a/src/app/shared/search/search-results/search-results.component.ts +++ b/src/app/shared/search/search-results/search-results.component.ts @@ -67,6 +67,11 @@ export class SearchResultsComponent { @Input() context: Context; + /** + * Option for hiding the pagination detail + */ + @Input() hidePaginationDetail = false; + @Input() selectionConfig: {repeatable: boolean, listId: string}; @Output() deselectObject: EventEmitter = new EventEmitter(); From 7a904f9bf7ec11ea5daf9586bcc3ca7ff71f63e7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 5 Dec 2019 14:06:29 +0100 Subject: [PATCH 39/77] 67611: Refactored import button to separate component within object-collection + moved calling of import modal to external-source tab --- ...try-list-submission-element.component.html | 7 -- ...entry-list-submission-element.component.ts | 26 ----- ...elation-external-source-tab.component.html | 5 +- ...-relation-external-source-tab.component.ts | 54 +++++++++- ...l-source-entry-import-modal.component.html | 2 +- ...nal-source-entry-import-modal.component.ts | 99 +++++++++++++++++-- .../object-collection.component.html | 3 + .../object-collection.component.ts | 15 +++ ...mportable-list-item-control.component.html | 7 ++ .../importable-list-item-control.component.ts | 26 +++++ .../object-list/object-list.component.html | 5 + .../object-list/object-list.component.ts | 16 +++ src/app/shared/shared.module.ts | 4 +- 13 files changed, 223 insertions(+), 46 deletions(-) create mode 100644 src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html create mode 100644 src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.ts diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html index 4df06bc0e6..5f34d8ccd0 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -1,11 +1,4 @@
        - -
        -
        {{object.display}}
        diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index 07d691a5cb..8891a179c3 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -6,10 +6,6 @@ import { Context } from '../../../../../core/shared/context.model'; import { Component, Inject, OnInit } from '@angular/core'; import { Metadata } from '../../../../../core/shared/metadata.utils'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { ExternalSourceEntryImportModalComponent } from '../../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; -import { DsDynamicLookupRelationExternalSourceTabComponent } from '../../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; -import { hasValue } from '../../../../../shared/empty.util'; @listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) @Component({ @@ -23,29 +19,7 @@ export class ExternalSourceEntryListSubmissionElementComponent extends AbstractL */ uri: MetadataValue; - /** - * The modal for importing the entry - */ - modalRef: NgbModalRef; - - constructor(@Inject(DsDynamicLookupRelationExternalSourceTabComponent) private externalSourceTab: DsDynamicLookupRelationExternalSourceTabComponent, - private modalService: NgbModal) { - super(); - } - ngOnInit(): void { this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); } - - import(): void { - this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, { - size: 'lg', - container: 'ds-dynamic-lookup-relation-modal' - }); - const modalComp = this.modalRef.componentInstance; - modalComp.externalSourceEntry = this.object; - if (hasValue(this.externalSourceTab) && hasValue(this.externalSourceTab.relationship)) { - modalComp.relationship = this.externalSourceTab.relationship; - } - } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html index a9ae77a9f5..04737c44e4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -14,8 +14,9 @@ [config]="initialPagination" [hideGear]="true" [context]="context" - (deselectObject)="deselectObject.emit($event)" - (selectObject)="selectObject.emit($event)"> + [importable]="true" + [importConfig]="importConfig" + (importObject)="import($event)"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index d884f5f63d..18dfa4277f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { Router } from '@angular/router'; @@ -15,6 +15,11 @@ import { ListableObject } from '../../../../../object-collection/shared/listable import { fadeIn, fadeInOut } from '../../../../../animations/fade'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { hasValue } from '../../../../../empty.util'; +import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -32,7 +37,7 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' ] }) -export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit { +export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit, OnDestroy { @Input() label: string; @Input() listId: string; @Input() relationship: RelationshipOptions; @@ -56,9 +61,28 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit */ entriesRD$: Observable>>; + /** + * Config to use for the import buttons + */ + importConfig = { + buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title' + }; + + /** + * The modal for importing the entry + */ + modalRef: NgbModalRef; + + /** + * Subscription to the modal's importedObject event-emitter + */ + importObjectSub: Subscription; + constructor(private router: Router, public searchConfigService: SearchConfigurationService, - private externalSourceService: ExternalSourceService) { + private externalSourceService: ExternalSourceService, + private modalService: NgbModal, + private selectableListService: SelectableListService) { } ngOnInit(): void { @@ -67,4 +91,28 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions)) ) } + + /** + * Start the import of an entry by opening up an import modal window + * @param entry The entry to import + */ + import(entry) { + this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, { + size: 'lg', + container: 'ds-dynamic-lookup-relation-modal' + }); + const modalComp = this.modalRef.componentInstance; + modalComp.externalSourceEntry = entry; + modalComp.relationship = this.relationship; + this.importObjectSub = modalComp.importedObject.subscribe((object) => { + this.selectableListService.selectSingle(this.listId, object); + this.selectObject.emit(object); + }); + } + + ngOnDestroy(): void { + if (hasValue(this.importObjectSub)) { + this.importObjectSub.unsubscribe(); + } + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html index f055e617e3..348b7eae18 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -26,7 +26,7 @@ [selectionConfig]="{ repeatable: false, listId: entityListId }" [linkType]="linkTypes.ExternalLink" [context]="context" - (deselectObject)="deselectEntity($event)" + (deselectObject)="deselectEntity()" (selectObject)="selectEntity($event)">
        diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index 6f17ddc529..f8b7b07184 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, EventEmitter, OnInit } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model'; import { MetadataValue } from '../../../../../../../core/shared/metadata.models'; @@ -14,6 +14,8 @@ import { PaginatedSearchOptions } from '../../../../../../search/paginated-searc import { CollectionElementLinkType } from '../../../../../../object-collection/collection-element-link.type'; import { Context } from '../../../../../../../core/shared/context.model'; import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; +import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model'; +import { take } from 'rxjs/operators'; /** * The possible types of import for the external entry @@ -87,6 +89,21 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { */ selectedImportType = ImportType.None; + /** + * The selected local entity + */ + selectedEntity: ListableObject; + + /** + * The selected local authority + */ + selectedAuthority: ListableObject; + + /** + * An object has been imported, send it to the parent component + */ + importedObject: EventEmitter = new EventEmitter(); + constructor(public modal: NgbActiveModal, public lookupRelationService: LookupRelationService, private selectService: SelectableListService) { @@ -109,14 +126,64 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { * Perform the import of the external entry */ import() { - console.log('TODO: Import'); + switch (this.selectedImportType) { + case ImportType.LocalEntity : { + this.importLocalEntity(); + break; + } + case ImportType.NewEntity : { + this.importNewEntity(); + break; + } + case ImportType.LocalAuthority : { + this.importLocalAuthority(); + break; + } + case ImportType.NewAuthority : { + this.importNewAuthority(); + break; + } + } + this.selectedImportType = ImportType.None; + this.deselectAllLists(); + this.close(); + } + + /** + * Import the selected local entity + */ + importLocalEntity() { + if (this.selectedEntity !== undefined) { + this.importedObject.emit(this.selectedEntity); + } + } + + /** + * Create and import a new entity from the external entry + */ + importNewEntity() { + this.importedObject.emit(this.externalSourceEntry); + } + + /** + * Import the selected local authority + */ + importLocalAuthority() { + // TODO: Implement ability to import local authorities + } + + /** + * Create and import a new authority from the external entry + */ + importNewAuthority() { + // TODO: Implement ability to import new authorities } /** * Deselected a local entity - * @param event */ - deselectEntity(event) { + deselectEntity() { + this.selectedEntity = undefined; if (this.selectedImportType === ImportType.LocalEntity) { this.selectedImportType = ImportType.None; } @@ -124,9 +191,10 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { /** * Selected a local entity - * @param event + * @param entity */ - selectEntity(event) { + selectEntity(entity) { + this.selectedEntity = entity; this.selectedImportType = ImportType.LocalEntity; } @@ -142,6 +210,25 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { } } + /** + * Deselected a local authority + */ + deselectAuthority() { + this.selectedAuthority = undefined; + if (this.selectedImportType === ImportType.LocalAuthority) { + this.selectedImportType = ImportType.None; + } + } + + /** + * Selected a local authority + * @param authority + */ + selectAuthority(authority) { + this.selectedAuthority = authority; + this.selectedImportType = ImportType.LocalAuthority; + } + /** * Selected/deselected the new authority option */ diff --git a/src/app/shared/object-collection/object-collection.component.html b/src/app/shared/object-collection/object-collection.component.html index 499b8e03c9..e696170a6f 100644 --- a/src/app/shared/object-collection/object-collection.component.html +++ b/src/app/shared/object-collection/object-collection.component.html @@ -15,6 +15,9 @@ (sortFieldChange)="onSortFieldChange($event)" [selectable]="selectable" [selectionConfig]="selectionConfig" + [importable]="importable" + [importConfig]="importConfig" + (importObject)="importObject.emit($event)" *ngIf="(currentMode$ | async) === viewModeEnum.ListElement"> diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index 58de49ba40..f39bf07123 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -53,6 +53,21 @@ export class ObjectCollectionComponent implements OnInit { @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * Whether or not to add an import button to the object elements + */ + @Input() importable = false; + + /** + * The config to use for the import button + */ + @Input() importConfig: { buttonLabel: string }; + + /** + * Send an import event to the parent component + */ + @Output() importObject: EventEmitter = new EventEmitter(); + /** * The link type of the rendered list elements */ diff --git a/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html new file mode 100644 index 0000000000..ca3b086653 --- /dev/null +++ b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.html @@ -0,0 +1,7 @@ +
        + +
        diff --git a/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.ts b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.ts new file mode 100644 index 0000000000..f381a02d86 --- /dev/null +++ b/src/app/shared/object-collection/shared/importable-list-item-control/importable-list-item-control.component.ts @@ -0,0 +1,26 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ListableObject } from '../listable-object.model'; + +@Component({ + selector: 'ds-importable-list-item-control', + templateUrl: './importable-list-item-control.component.html' +}) +/** + * Component adding an import button to a list item + */ +export class ImportableListItemControlComponent { + /** + * The item or metadata to determine the component for + */ + @Input() object: ListableObject; + + /** + * Extra configuration for the import button + */ + @Input() importConfig: { buttonLabel: string }; + + /** + * Output the object to import + */ + @Output() importObject: EventEmitter = new EventEmitter(); +} diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index d0cfb1a9d8..5f6b1d1ec8 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -20,6 +20,11 @@ (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)"> + + + diff --git a/src/app/shared/object-list/object-list.component.ts b/src/app/shared/object-list/object-list.component.ts index 1efd715084..5ebaae668c 100644 --- a/src/app/shared/object-list/object-list.component.ts +++ b/src/app/shared/object-list/object-list.component.ts @@ -79,6 +79,16 @@ export class ObjectListComponent { */ @Input() hidePaginationDetail = false; + /** + * Whether or not to add an import button to the object + */ + @Input() importable = false; + + /** + * Config used for the import button + */ + @Input() importConfig: { importLabel: string }; + /** * The current listable objects */ @@ -137,6 +147,12 @@ export class ObjectListComponent { @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); + + /** + * Send an import event to the parent component + */ + @Output() importObject: EventEmitter = new EventEmitter(); + /** * An event fired when the sort field is changed. * Event's payload equals to the newly selected sort field. diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 230bb1f073..6afa15d0f7 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -176,6 +176,7 @@ import { MetadataRepresentationListComponent } from '../+item-page/simple/metada import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; import { ExternalSourceEntryImportModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component'; +import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -333,7 +334,8 @@ const COMPONENTS = [ CollectionSelectComponent, MetadataRepresentationLoaderComponent, SelectableListItemControlComponent, - ExternalSourceEntryImportModalComponent + ExternalSourceEntryImportModalComponent, + ImportableListItemControlComponent ]; const ENTRY_COMPONENTS = [ From fda083137a2854daaf780a523d39a3fb699937c8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 5 Dec 2019 15:48:54 +0100 Subject: [PATCH 40/77] 67611: Import external entry as new entity pt1 --- resources/i18n/en.json5 | 2 ++ src/app/core/data/item-data.service.ts | 29 +++++++++++++++++++ ...ynamic-form-control-container.component.ts | 14 +++++++-- ...namic-lookup-relation-modal.component.html | 2 ++ ...dynamic-lookup-relation-modal.component.ts | 1 + ...-relation-external-source-tab.component.ts | 6 ++++ ...l-source-entry-import-modal.component.html | 12 ++++++-- ...nal-source-entry-import-modal.component.ts | 27 +++++++++++++++-- 8 files changed, 84 insertions(+), 9 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index fe7949d8b1..16d5d64bd9 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1533,6 +1533,8 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.cancel": "Cancel", + "submission.sections.describe.relationship-lookup.external-source.import-modal.collection": "Select a collection to import new entries to", + "submission.sections.describe.relationship-lookup.external-source.import-modal.entities": "Entities", "submission.sections.describe.relationship-lookup.external-source.import-modal.entities.new": "Import as a new local entity", diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index e616cb8020..b5f16227a8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -37,6 +37,7 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { Collection } from '../shared/collection.model'; import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; @Injectable() export class ItemDataService extends DataService { @@ -247,4 +248,32 @@ export class ItemDataService extends DataService { map((request: RequestEntry) => request.response) ); } + + /** + * Import an external source entry into a collection + * @param externalSourceEntry + * @param collectionId + */ + public importExternalSourceEntry(externalSourceEntry: ExternalSourceEntry, collectionId: string): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint(this.linkPath).pipe(map((href) => `${href}?owningCollection=${collectionId}`)); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new PostRequest(requestId, href, externalSourceEntry.self, options); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index b78385dc62..d6e3a97a11 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -101,6 +101,7 @@ import { ItemSearchResult } from '../../../object-collection/shared/item-search- import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; import * as uuidv4 from 'uuid/v4'; +import { Collection } from '../../../../core/shared/collection.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -188,6 +189,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo hasRelationLookup: boolean; modalRef: NgbModalRef; item: Item; + collection: Collection; listId: string; searchConfig: string; selectedValues$: Observable (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + getRemoteDataPayload() + ); + + const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); this.subs.push(item$.subscribe((item) => this.item = item)); + this.subs.push(collection$.subscribe((collection) => this.collection = collection)); this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), @@ -325,6 +332,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.label = this.model.label; modalComp.metadataFields = this.model.metadataFields; modalComp.item = this.item; + modalComp.collection = this.collection; } removeSelection(object: SearchResult) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 4c87ac8bdb..1773661486 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -26,6 +26,8 @@ ; context: Context; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index 18dfa4277f..dfdb1508fc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -20,6 +20,8 @@ import { ExternalSourceEntryImportModalComponent } from './external-source-entry import { Subscription } from 'rxjs/internal/Subscription'; import { hasValue } from '../../../../../empty.util'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; +import { Item } from '../../../../../../core/shared/item.model'; +import { Collection } from '../../../../../../core/shared/collection.model'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -40,6 +42,8 @@ import { SelectableListService } from '../../../../../object-list/selectable-lis export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit, OnDestroy { @Input() label: string; @Input() listId: string; + @Input() item: Item; + @Input() collection: Collection; @Input() relationship: RelationshipOptions; @Input() repeatable: boolean; @Input() context: Context; @@ -103,6 +107,8 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit }); const modalComp = this.modalRef.componentInstance; modalComp.externalSourceEntry = entry; + modalComp.item = this.item; + modalComp.collection = this.collection; modalComp.relationship = this.relationship; this.importObjectSub = modalComp.importedObject.subscribe((object) => { this.selectableListService.selectSingle(this.listId, object); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html index 348b7eae18..ffb3aabde8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -13,7 +13,13 @@

        {{ ('submission.sections.describe.relationship-lookup.external-source.import-modal.select' | translate) }}

        -
        +
        +
        + + +
        +
        +
        {{ ('submission.sections.describe.relationship-lookup.external-source.import-modal.entities' | translate) }}
        -
        +
        @@ -37,7 +43,7 @@
        {{ ('submission.sections.describe.relationship-lookup.external-source.import-modal.authority' | translate) }}
        -
        +
        diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index f8b7b07184..acd1fa59ea 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -15,7 +15,8 @@ import { CollectionElementLinkType } from '../../../../../../object-collection/c import { Context } from '../../../../../../../core/shared/context.model'; import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model'; -import { take } from 'rxjs/operators'; +import { Collection } from '../../../../../../../core/shared/collection.model'; +import { ItemDataService } from '../../../../../../../core/data/item-data.service'; /** * The possible types of import for the external entry @@ -39,6 +40,21 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { */ externalSourceEntry: ExternalSourceEntry; + /** + * The item in submission + */ + item: Item; + + /** + * The collection the user is submitting in + */ + collection: Collection; + + /** + * The ID of the collection to import entries to + */ + collectionId: string; + /** * The current relationship-options used for filtering results */ @@ -106,13 +122,15 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { constructor(public modal: NgbActiveModal, public lookupRelationService: LookupRelationService, - private selectService: SelectableListService) { + private selectService: SelectableListService, + private itemService: ItemDataService) { } ngOnInit(): void { this.uri = Metadata.first(this.externalSourceEntry.metadata, 'dc.identifier.uri'); this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: 'sarah' })); this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions); + this.collectionId = this.collection.id; } /** @@ -162,7 +180,10 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { * Create and import a new entity from the external entry */ importNewEntity() { - this.importedObject.emit(this.externalSourceEntry); + console.log(this.collection); + this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).subscribe((response) => { + console.log(response); + }); } /** From f0e38e9e8c7085fc2ec25a000651811bf92984fb Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 5 Dec 2019 17:31:13 +0100 Subject: [PATCH 41/77] 67611: Console error fix, notification on adding local entity and JSDocs --- resources/i18n/en.json5 | 3 ++ ...namic-lookup-relation-modal.component.html | 1 - ...-relation-external-source-tab.component.ts | 50 +++++++++++++++++-- ...nal-source-entry-import-modal.component.ts | 11 +++- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 16d5d64bd9..7fa9c96010 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1523,8 +1523,11 @@ "submission.general.save-later": "Save for later", + "submission.sections.describe.relationship-lookup.close": "Close", + "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", + "submission.sections.describe.relationship-lookup.external-source.import-button-title": "Import remote entry", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 1773661486..06e45ecb92 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -29,7 +29,6 @@ [item]="item" [collection]="collection" [relationship]="relationshipOptions" - [repeatable]="repeatable" [context]="context" [externalSource]="source" (selectObject)="select($event)" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index dfdb1508fc..54cd4e2f86 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -22,6 +22,8 @@ import { hasValue } from '../../../../../empty.util'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { Item } from '../../../../../../core/shared/item.model'; import { Collection } from '../../../../../../core/shared/collection.model'; +import { NotificationsService } from '../../../../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -38,18 +40,49 @@ import { Collection } from '../../../../../../core/shared/collection.model'; fadeInOut ] }) - +/** + * Component rendering the tab content of an external source during submission lookup + * Shows a list of entries matching the current search query with the option to import them into the repository + */ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit, OnDestroy { - @Input() label: string; + /** + * The ID of the list of selected entries + */ @Input() listId: string; + + /** + * The item in submission + */ @Input() item: Item; + + /** + * The collection the user is submitting an item into + */ @Input() collection: Collection; + + /** + * The relationship-options for the current lookup + */ @Input() relationship: RelationshipOptions; - @Input() repeatable: boolean; + + /** + * The context to displaying lists for + */ @Input() context: Context; + + /** + * Emit an event when an object has been deselected + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Emit an event when an object has been selected (or imported) + */ @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * The initial pagination options + */ initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'submission-external-source-relation-list', pageSize: 5 @@ -86,9 +119,14 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit public searchConfigService: SearchConfigurationService, private externalSourceService: ExternalSourceService, private modalService: NgbModal, - private selectableListService: SelectableListService) { + private selectableListService: SelectableListService, + private notificationsService: NotificationsService, + private translateService: TranslateService) { } + /** + * Get the entries for the selected external source + */ ngOnInit(): void { this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( switchMap((searchOptions: PaginatedSearchOptions) => @@ -112,10 +150,14 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit modalComp.relationship = this.relationship; this.importObjectSub = modalComp.importedObject.subscribe((object) => { this.selectableListService.selectSingle(this.listId, object); + this.notificationsService.success(this.translateService.get('submission.sections.describe.relationship-lookup.external-source.added')); this.selectObject.emit(object); }); } + /** + * Unsubscribe from open subscriptions + */ ngOnDestroy(): void { if (hasValue(this.importObjectSub)) { this.importObjectSub.unsubscribe(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index acd1fa59ea..2107021d40 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -17,6 +17,7 @@ import { SelectableListService } from '../../../../../../object-list/selectable- import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model'; import { Collection } from '../../../../../../../core/shared/collection.model'; import { ItemDataService } from '../../../../../../../core/data/item-data.service'; +import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model'; /** * The possible types of import for the external entry @@ -34,6 +35,12 @@ export enum ImportType { styleUrls: ['./external-source-entry-import-modal.component.scss'], templateUrl: './external-source-entry-import-modal.component.html' }) +/** + * Component to display a modal window for importing an external source entry + * Shows information about the selected entry and a selectable list of local entities and authorities with similar names + * and the ability to add one of those results to the selection instead of the external entry. + * The other option is to import the external entry as a new entity or authority into the repository. + */ export class ExternalSourceEntryImportModalComponent implements OnInit { /** * The external source entry @@ -128,7 +135,8 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { ngOnInit(): void { this.uri = Metadata.first(this.externalSourceEntry.metadata, 'dc.identifier.uri'); - this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: 'sarah' })); + const pagination = Object.assign(new PaginationComponentOptions(), { id: 'external-entry-import', pageSize: 5 }); + this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: this.externalSourceEntry.value, pagination: pagination })); this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions); this.collectionId = this.collection.id; } @@ -180,7 +188,6 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { * Create and import a new entity from the external entry */ importNewEntity() { - console.log(this.collection); this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).subscribe((response) => { console.log(response); }); From cdbb9b620360acb051c80eb983c0ee1f0de4b8f1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 6 Dec 2019 12:59:01 +0100 Subject: [PATCH 42/77] 67478: External source entries loading on search-option change --- .../dynamic-lookup-relation-external-source-tab.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index efee24b8ef..a8332dd3a1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -8,7 +8,7 @@ import { RemoteData } from '../../../../../../core/data/remote-data'; import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; import { ExternalSource } from '../../../../../../core/shared/external-source.model'; -import { switchMap } from 'rxjs/operators'; +import { startWith, switchMap } from 'rxjs/operators'; import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; import { Context } from '../../../../../../core/shared/context.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; @@ -62,7 +62,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit ngOnInit(): void { this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( switchMap((searchOptions: PaginatedSearchOptions) => - this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions)) + this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined))) ) } } From 1402dd9129156dddaa86f8421ccddcc8db2f5cc8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 6 Dec 2019 16:52:30 +0100 Subject: [PATCH 43/77] 67611: Import new external entity and add to selection + message changes/refactoring --- resources/i18n/en.json5 | 34 +++++++++++++++++-- src/app/core/data/item-data.service.ts | 10 ++++-- src/app/core/data/lookup-relation.service.ts | 11 +++++- ...namic-lookup-relation-modal.component.html | 4 +-- ...dynamic-lookup-relation-modal.component.ts | 9 +++++ ...-relation-external-source-tab.component.ts | 33 ++++++++---------- ...l-source-entry-import-modal.component.html | 22 ++++++------ ...nal-source-entry-import-modal.component.ts | 33 ++++++++++++++++-- 8 files changed, 116 insertions(+), 40 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 76ffe955a7..cee53de8ec 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1546,7 +1546,19 @@ "submission.sections.describe.relationship-lookup.external-source.added": "Successfully added local entry to the selection", - "submission.sections.describe.relationship-lookup.external-source.import-button-title": "Import remote entry", + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Author": "Import remote author", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal": "Import remote journal", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Issue": "Import remote journal issue", + + "submission.sections.describe.relationship-lookup.external-source.import-button-title.Journal Volume": "Import remote journal volume", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.title": "Import Remote Author", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.local-entity": "Successfully added local author to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Author.added.new-entity": "Successfully imported and added external author to the selection", "submission.sections.describe.relationship-lookup.external-source.import-modal.authority": "Authority", @@ -1570,9 +1582,25 @@ "submission.sections.describe.relationship-lookup.external-source.import-modal.import": "Import", - "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.title": "Import Remote Journal", - "submission.sections.describe.relationship-lookup.external-source.import-modal.title": "Import Remote Entry", + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.local-entity": "Successfully added local journal to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal.added.new-entity": "Successfully imported and added external journal to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.title": "Import Remote Journal Issue", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.local-entity": "Successfully added local journal issue to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Issue.added.new-entity": "Successfully imported and added external journal issue to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.title": "Import Remote Journal Volume", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.local-entity": "Successfully added local journal volume to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.Journal Volume.added.new-entity": "Successfully imported and added external journal volume to the selection", + + "submission.sections.describe.relationship-lookup.external-source.import-modal.select": "Select a local match:", "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index b71b778993..cd7e70dc32 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -254,7 +254,7 @@ export class ItemDataService extends DataService { * @param externalSourceEntry * @param collectionId */ - public importExternalSourceEntry(externalSourceEntry: ExternalSourceEntry, collectionId: string): Observable { + public importExternalSourceEntry(externalSourceEntry: ExternalSourceEntry, collectionId: string): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); @@ -273,7 +273,13 @@ export class ItemDataService extends DataService { return this.requestService.getByUUID(requestId).pipe( find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) + getResponseFromEntry(), + map((response: any) => { + if (isNotEmpty(response.resourceSelfLinks)) { + return response.resourceSelfLinks[0]; + } + }), + switchMap((selfLink: string) => this.findByHref(selfLink)) ); } diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 5a36474a66..42cf3afdc5 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -15,6 +15,7 @@ import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/opera import { Injectable } from '@angular/core'; import { ExternalSource } from '../shared/external-source.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { RequestService } from './request.service'; /** * A service for retrieving local and external entries information during a relation lookup @@ -35,7 +36,8 @@ export class LookupRelationService { }); constructor(protected externalSourceService: ExternalSourceService, - protected searchService: SearchService) { + protected searchService: SearchService, + protected requestService: RequestService) { } /** @@ -91,4 +93,11 @@ export class LookupRelationService { startWith(0) ); } + + /** + * Remove cached requests from local results + */ + removeLocalResultsCache() { + this.searchService.getEndpoint().subscribe((href) => this.requestService.removeByHrefSubstring(href)); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 06e45ecb92..328cdc6763 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -25,14 +25,14 @@ [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : {count: (totalExternal$ | async)[idx]}"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index c49a228c01..4845ffb398 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -179,6 +179,15 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ) } + /** + * Called when an external object has been imported, resets the total values and adds the object to the selected list + * @param object + */ + imported(object) { + this.setTotals(); + this.select(object); + } + /** * Calculate and set the total entries available for each tab */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index 65645e0739..ec036e4469 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -22,8 +22,6 @@ import { hasValue } from '../../../../../empty.util'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { Item } from '../../../../../../core/shared/item.model'; import { Collection } from '../../../../../../core/shared/collection.model'; -import { NotificationsService } from '../../../../../notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -45,6 +43,11 @@ import { TranslateService } from '@ngx-translate/core'; * Shows a list of entries matching the current search query with the option to import them into the repository */ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit, OnDestroy { + /** + * The label to use for all messages (added to the end of relevant i18n keys) + */ + @Input() label: string; + /** * The ID of the list of selected entries */ @@ -71,14 +74,9 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit @Input() context: Context; /** - * Emit an event when an object has been deselected + * Emit an event when an object has been imported (or selected from similar local entries) */ - @Output() deselectObject: EventEmitter = new EventEmitter(); - - /** - * Emit an event when an object has been selected (or imported) - */ - @Output() selectObject: EventEmitter = new EventEmitter(); + @Output() importedObject: EventEmitter = new EventEmitter(); /** * The initial pagination options @@ -101,9 +99,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit /** * Config to use for the import buttons */ - importConfig = { - buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title' - }; + importConfig; /** * The modal for importing the entry @@ -119,9 +115,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit public searchConfigService: SearchConfigurationService, private externalSourceService: ExternalSourceService, private modalService: NgbModal, - private selectableListService: SelectableListService, - private notificationsService: NotificationsService, - private translateService: TranslateService) { + private selectableListService: SelectableListService) { } /** @@ -131,7 +125,10 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( switchMap((searchOptions: PaginatedSearchOptions) => this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined))) - ) + ); + this.importConfig = { + buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label + }; } /** @@ -148,10 +145,10 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit modalComp.item = this.item; modalComp.collection = this.collection; modalComp.relationship = this.relationship; + modalComp.label = this.label; this.importObjectSub = modalComp.importedObject.subscribe((object) => { this.selectableListService.selectSingle(this.listId, object); - this.notificationsService.success(this.translateService.get('submission.sections.describe.relationship-lookup.external-source.added')); - this.selectObject.emit(object); + this.importedObject.emit(object); }); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html index ffb3aabde8..b88fea8e41 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -1,26 +1,26 @@ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index 2107021d40..8ca8305085 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -18,6 +18,11 @@ import { ListableObject } from '../../../../../../object-collection/shared/lista import { Collection } from '../../../../../../../core/shared/collection.model'; import { ItemDataService } from '../../../../../../../core/data/item-data.service'; import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../../../core/shared/operators'; +import { take } from 'rxjs/operators'; +import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model'; +import { NotificationsService } from '../../../../../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * The possible types of import for the external entry @@ -42,6 +47,16 @@ export enum ImportType { * The other option is to import the external entry as a new entity or authority into the repository. */ export class ExternalSourceEntryImportModalComponent implements OnInit { + /** + * The prefix for every i18n key within this modal + */ + labelPrefix = 'submission.sections.describe.relationship-lookup.external-source.import-modal.'; + + /** + * The label to use for all messages (added to the end of relevant i18n keys) + */ + label: string; + /** * The external source entry */ @@ -130,7 +145,9 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { constructor(public modal: NgbActiveModal, public lookupRelationService: LookupRelationService, private selectService: SelectableListService, - private itemService: ItemDataService) { + private itemService: ItemDataService, + private notificationsService: NotificationsService, + private translateService: TranslateService) { } ngOnInit(): void { @@ -180,6 +197,7 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { */ importLocalEntity() { if (this.selectedEntity !== undefined) { + this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.local-entity')); this.importedObject.emit(this.selectedEntity); } } @@ -188,8 +206,17 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { * Create and import a new entity from the external entry */ importNewEntity() { - this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).subscribe((response) => { - console.log(response); + this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + take(1) + ).subscribe((item: Item) => { + this.lookupRelationService.removeLocalResultsCache(); + const searchResult = Object.assign(new ItemSearchResult(), { + indexableObject: item + }); + this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.new-entity')); + this.importedObject.emit(searchResult); }); } From 9246cce7de62a9287de17be1ca64d449e7a9bd8d Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 6 Dec 2019 17:54:52 +0100 Subject: [PATCH 44/77] 67611: Test cases --- src/app/core/data/item-data.service.spec.ts | 21 ++++++++ .../core/data/lookup-relation.service.spec.ts | 22 ++++++-- ...tion-external-source-tab.component.spec.ts | 54 ++++++++++++++----- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 44c5f48cfe..ebe4cbfc0a 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -23,6 +23,7 @@ import { HttpClient } from '@angular/common/http'; import { RequestEntry } from './request.reducer'; import { of as observableOf } from 'rxjs'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -202,4 +203,24 @@ describe('ItemDataService', () => { }); }); + describe('importExternalSourceEntry', () => { + let result; + + const externalSourceEntry = Object.assign(new ExternalSourceEntry(), { + display: 'John, Doe', + value: 'John, Doe', + self: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' + }); + + beforeEach(() => { + service = initTestService(); + spyOn(requestService, 'configure'); + result = service.importExternalSourceEntry(externalSourceEntry, 'collection-id'); + }); + + it('should configure a POST request', () => { + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest), undefined)); + }); + }); + }); diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index 321fd8d218..e12f81f15f 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -10,11 +10,14 @@ import { SearchResult } from '../../shared/search/search-result.model'; import { Item } from '../shared/item.model'; import { skip, take } from 'rxjs/operators'; import { ExternalSource } from '../shared/external-source.model'; +import { RequestService } from './request.service'; +import { of as observableOf } from 'rxjs'; -describe('LookupRelationService', () => { +fdescribe('LookupRelationService', () => { let service: LookupRelationService; let externalSourceService: ExternalSourceService; let searchService: SearchService; + let requestService: RequestService; const totalExternal = 8; const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); @@ -35,15 +38,18 @@ describe('LookupRelationService', () => { name: 'orcidV2', hierarchical: false }); + const searchServiceEndpoint = 'http://test-rest.com/server/api/core/search'; function init() { externalSourceService = jasmine.createSpyObj('externalSourceService', { getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) }); searchService = jasmine.createSpyObj('searchService', { - search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)) + search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)), + getEndpoint: observableOf(searchServiceEndpoint) }); - service = new LookupRelationService(externalSourceService, searchService); + requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']); + service = new LookupRelationService(externalSourceService, searchService, requestService); } beforeEach(() => { @@ -113,4 +119,14 @@ describe('LookupRelationService', () => { }); }); }); + + describe('removeLocalResultsCache', () => { + beforeEach(() => { + service.removeLocalResultsCache(); + }); + + it('should call requestService\'s removeByHrefSubstring with the search endpoint', () => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(searchServiceEndpoint); + }); + }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts index 62327e236e..e35b144372 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { VarDirective } from '../../../../../utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { of as observableOf } from 'rxjs/internal/observable/of'; @@ -18,12 +18,20 @@ import { ExternalSource } from '../../../../../../core/shared/external-source.mo import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; +import { Item } from '../../../../../../core/shared/item.model'; +import { Collection } from '../../../../../../core/shared/collection.model'; +import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component'; -describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { +fdescribe('DsDynamicLookupRelationExternalSourceTabComponent', () => { let component: DsDynamicLookupRelationExternalSourceTabComponent; let fixture: ComponentFixture; let pSearchOptions; let externalSourceService; + let selectableListService; + let modalService; const externalSource = { id: 'orcidV2', @@ -68,6 +76,10 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { } }) ] as ExternalSourceEntry[]; + const item = Object.assign(new Item(), { id: 'submission-item' }); + const collection = Object.assign(new Collection(), { id: 'submission-collection' }); + const relationship = Object.assign(new RelationshipOptions(), { relationshipType: 'isAuthorOfPublication' }); + const label = 'Author'; function init() { pSearchOptions = new PaginatedSearchOptions({ @@ -76,20 +88,22 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { externalSourceService = jasmine.createSpyObj('externalSourceService', { getExternalSourceEntries: createSuccessfulRemoteDataObject$(createPaginatedList(externalEntries)) }); + selectableListService = jasmine.createSpyObj('selectableListService', ['selectSingle']); } beforeEach(async(() => { init(); TestBed.configureTestingModule({ declarations: [DsDynamicLookupRelationExternalSourceTabComponent, VarDirective], - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), BrowserAnimationsModule], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot(), BrowserAnimationsModule], providers: [ { provide: SearchConfigurationService, useValue: { paginatedSearchOptions: observableOf(pSearchOptions) } }, - { provide: ExternalSourceService, useValue: externalSourceService } + { provide: ExternalSourceService, useValue: externalSourceService }, + { provide: SelectableListService, useValue: selectableListService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -99,13 +113,18 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { fixture = TestBed.createComponent(DsDynamicLookupRelationExternalSourceTabComponent); component = fixture.componentInstance; component.externalSource = externalSource; + component.item = item; + component.collection = collection; + component.relationship = relationship; + component.label = label; + modalService = (component as any).modalService; fixture.detectChanges(); }); describe('when the external entries finished loading successfully', () => { it('should display a ds-viewable-collection component', () => { - const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); - expect(collection).toBeDefined(); + const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(viewableCollection).toBeDefined(); }); }); @@ -116,8 +135,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { }); it('should not display a ds-viewable-collection component', () => { - const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); - expect(collection).toBeNull(); + const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(viewableCollection).toBeNull(); }); it('should display a ds-loading component', () => { @@ -133,8 +152,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { }); it('should not display a ds-viewable-collection component', () => { - const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); - expect(collection).toBeNull(); + const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(viewableCollection).toBeNull(); }); it('should display a ds-error component', () => { @@ -150,8 +169,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { }); it('should not display a ds-viewable-collection component', () => { - const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); - expect(collection).toBeNull(); + const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(viewableCollection).toBeNull(); }); it('should display a message the list is empty', () => { @@ -159,4 +178,15 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { expect(empty).not.toBeNull(); }); }); + + describe('import', () => { + beforeEach(() => { + spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter() }) })); + component.import(externalEntries[0]); + }); + + it('should open a new ExternalSourceEntryImportModalComponent', () => { + expect(modalService.open).toHaveBeenCalledWith(ExternalSourceEntryImportModalComponent, jasmine.any(Object)) + }); + }); }); From 6914000e1971502266bd87480bcd0e4be2d98388 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 9 Dec 2019 11:59:55 +0100 Subject: [PATCH 45/77] 67611: Remaining test cases --- .../core/data/lookup-relation.service.spec.ts | 2 +- ...tion-external-source-tab.component.spec.ts | 2 +- ...ource-entry-import-modal.component.spec.ts | 194 ++++++++++++++++++ 3 files changed, 196 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index e12f81f15f..c9fc7fc50d 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -13,7 +13,7 @@ import { ExternalSource } from '../shared/external-source.model'; import { RequestService } from './request.service'; import { of as observableOf } from 'rxjs'; -fdescribe('LookupRelationService', () => { +describe('LookupRelationService', () => { let service: LookupRelationService; let externalSourceService: ExternalSourceService; let searchService: SearchService; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts index e35b144372..00242ad9ce 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -25,7 +25,7 @@ import { Collection } from '../../../../../../core/shared/collection.model'; import { RelationshipOptions } from '../../../models/relationship-options.model'; import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component'; -fdescribe('DsDynamicLookupRelationExternalSourceTabComponent', () => { +describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { let component: DsDynamicLookupRelationExternalSourceTabComponent; let fixture: ComponentFixture; let pSearchOptions; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts index e69de29bb2..5248f95573 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts @@ -0,0 +1,194 @@ +import { ExternalSourceEntryImportModalComponent, ImportType } from './external-source-entry-import-modal.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { LookupRelationService } from '../../../../../../../core/data/lookup-relation.service'; +import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model'; +import { Item } from '../../../../../../../core/shared/item.model'; +import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../../../testing/utils'; +import { Collection } from '../../../../../../../core/shared/collection.model'; +import { RelationshipOptions } from '../../../../models/relationship-options.model'; +import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; +import { ItemDataService } from '../../../../../../../core/data/item-data.service'; +import { NotificationsService } from '../../../../../../notifications/notifications.service'; + +describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { + let component: ExternalSourceEntryImportModalComponent; + let fixture: ComponentFixture; + let lookupRelationService: LookupRelationService; + let selectService: SelectableListService; + let itemService: ItemDataService; + let notificationsService: NotificationsService; + let modalStub: NgbActiveModal; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + const label = 'Author'; + const relationship = Object.assign(new RelationshipOptions(), { relationshipType: 'isAuthorOfPublication' }); + const submissionCollection = Object.assign(new Collection(), { uuid: '9398affe-a977-4992-9a1d-6f00908a259f' }); + const submissionItem = Object.assign(new Item(), { uuid: '26224069-5f99-412a-9e9b-7912a7e35cb1' }); + const item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); + const item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); + const item3 = Object.assign(new Item(), { uuid: 'c3bcbff5-ec0c-4831-8e4c-94b9c933ccac' }); + const searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + const searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + const searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 }); + const importedItem = Object.assign(new Item(), { uuid: '5d0098fc-344a-4067-a57d-457092b72e82' }); + + function init() { + lookupRelationService = jasmine.createSpyObj('lookupRelationService', { + getLocalResults: createSuccessfulRemoteDataObject$(createPaginatedList([searchResult1, searchResult2, searchResult3])), + removeLocalResultsCache: {} + }); + selectService = jasmine.createSpyObj('selectService', ['deselectAll']); + notificationsService = jasmine.createSpyObj('notificationsService', ['success']); + itemService = jasmine.createSpyObj('itemService', { + importExternalSourceEntry: createSuccessfulRemoteDataObject$(importedItem) + }); + modalStub = jasmine.createSpyObj('modal', ['close']); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryImportModalComponent], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()], + providers: [ + { provide: LookupRelationService, useValue: lookupRelationService }, + { provide: SelectableListService, useValue: selectService }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: ItemDataService, useValue: itemService }, + { provide: NgbActiveModal, useValue: modalStub } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryImportModalComponent); + component = fixture.componentInstance; + component.externalSourceEntry = entry; + component.label = label; + component.relationship = relationship; + component.collection = submissionCollection; + component.item = submissionItem; + fixture.detectChanges(); + }); + + describe('close', () => { + beforeEach(() => { + component.close(); + }); + + it('should close the modal', () => { + expect(modalStub.close).toHaveBeenCalled(); + }); + }); + + describe('selectEntity', () => { + const entity = Object.assign(new Item(), { uuid: 'd8698de5-5b05-4ea4-9d02-da73803a50f9' }); + + beforeEach(() => { + component.selectEntity(entity); + }); + + it('should set selected entity', () => { + expect(component.selectedEntity).toBe(entity); + }); + + it('should set the import type to local entity', () => { + expect(component.selectedImportType).toEqual(ImportType.LocalEntity); + }); + }); + + describe('deselectEntity', () => { + const entity = Object.assign(new Item(), { uuid: 'd8698de5-5b05-4ea4-9d02-da73803a50f9' }); + + beforeEach(() => { + component.selectedImportType = ImportType.LocalEntity; + component.selectedEntity = entity; + component.deselectEntity(); + }); + + it('should remove the selected entity', () => { + expect(component.selectedEntity).toBeUndefined(); + }); + + it('should set the import type to none', () => { + expect(component.selectedImportType).toEqual(ImportType.None); + }); + }); + + describe('selectNewEntity', () => { + describe('when current import type is set to new entity', () => { + beforeEach(() => { + component.selectedImportType = ImportType.NewEntity; + component.selectNewEntity(); + }); + + it('should set the import type to none', () => { + expect(component.selectedImportType).toEqual(ImportType.None); + }); + }); + + describe('when current import type is not set to new entity', () => { + beforeEach(() => { + component.selectedImportType = ImportType.None; + component.selectNewEntity(); + }); + + it('should set the import type to new entity', () => { + expect(component.selectedImportType).toEqual(ImportType.NewEntity); + }); + + it('should deselect the entity and authority list', () => { + expect(selectService.deselectAll).toHaveBeenCalledWith(component.entityListId); + expect(selectService.deselectAll).toHaveBeenCalledWith(component.authorityListId); + }); + }); + }); + + describe('selectNewAuthority', () => { + describe('when current import type is set to new authority', () => { + beforeEach(() => { + component.selectedImportType = ImportType.NewAuthority; + component.selectNewAuthority(); + }); + + it('should set the import type to none', () => { + expect(component.selectedImportType).toEqual(ImportType.None); + }); + }); + + describe('when current import type is not set to new authority', () => { + beforeEach(() => { + component.selectedImportType = ImportType.None; + component.selectNewAuthority(); + }); + + it('should set the import type to new authority', () => { + expect(component.selectedImportType).toEqual(ImportType.NewAuthority); + }); + + it('should deselect the entity and authority list', () => { + expect(selectService.deselectAll).toHaveBeenCalledWith(component.entityListId); + expect(selectService.deselectAll).toHaveBeenCalledWith(component.authorityListId); + }); + }); + }); +}); From 64eb5db05bb3f67d6e0e07b9f74bc9eb1a0183b8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 19 Dec 2019 11:36:39 +0100 Subject: [PATCH 46/77] 67478: JSDocs intermediate commit --- .../normalized-external-source-entry.model.ts | 3 + .../normalized-external-source.model.ts | 3 + src/app/core/data/external-source.service.ts | 8 ++ src/app/core/data/relationship.service.ts | 27 +++++++ src/app/core/services/route.service.ts | 13 ++++ src/app/core/utilities/equatable.ts | 9 +++ ...entry-list-submission-element.component.ts | 3 + .../name-variant-modal.component.ts | 6 ++ .../disabled/dynamic-disabled.component.ts | 3 + ...dynamic-lookup-relation-modal.component.ts | 54 ++++++++++++- ...-relation-external-source-tab.component.ts | 30 +++++++- ...ic-lookup-relation-search-tab.component.ts | 75 ++++++++++++++++++- ...lookup-relation-selection-tab.component.ts | 35 ++++++++- .../models/relationship-options.model.ts | 3 + 14 files changed, 268 insertions(+), 4 deletions(-) diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts index 02abbb32d8..e8e3c695c3 100644 --- a/src/app/core/cache/models/normalized-external-source-entry.model.ts +++ b/src/app/core/cache/models/normalized-external-source-entry.model.ts @@ -4,6 +4,9 @@ import { ExternalSourceEntry } from '../../shared/external-source-entry.model'; import { mapsTo } from '../builders/build-decorators'; import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; +/** + * Normalized model class for an external source entry + */ @mapsTo(ExternalSourceEntry) @inheritSerialization(NormalizedObject) export class NormalizedExternalSourceEntry extends NormalizedObject { diff --git a/src/app/core/cache/models/normalized-external-source.model.ts b/src/app/core/cache/models/normalized-external-source.model.ts index dbcaeaf0de..fd9a42fb72 100644 --- a/src/app/core/cache/models/normalized-external-source.model.ts +++ b/src/app/core/cache/models/normalized-external-source.model.ts @@ -3,6 +3,9 @@ import { NormalizedObject } from './normalized-object.model'; import { ExternalSource } from '../../shared/external-source.model'; import { mapsTo } from '../builders/build-decorators'; +/** + * Normalized model class for an external source + */ @mapsTo(ExternalSource) @inheritSerialization(NormalizedObject) export class NormalizedExternalSource extends NormalizedObject { diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index 2ef1fa3d0e..c32c13a20f 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -21,6 +21,9 @@ import { PaginatedList } from './paginated-list'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +/** + * A service handling all external source requests + */ @Injectable() export class ExternalSourceService extends DataService { protected linkPath = 'externalsources'; @@ -38,6 +41,11 @@ export class ExternalSourceService extends DataService { super(); } + /** + * Get the endpoint to browse external sources + * @param options + * @param linkPath + */ getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { return this.halService.getEndpoint(linkPath); } diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 978eb3aa5f..4dfae732f4 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -288,24 +288,51 @@ export class RelationshipService extends DataService { ); } + /** + * Set a name variant for item with ID "itemID" part of list with ID "listID" + * @param listID ID of the list the item is a part of + * @param itemID ID of the item + * @param nameVariant A name variant for the item + */ public setNameVariant(listID: string, itemID: string, nameVariant: string) { this.appStore.dispatch(new SetNameVariantAction(listID, itemID, nameVariant)); } + /** + * Get the name variant for item with ID "itemID" part of list with ID "listID" + * @param listID ID of the list the item is a part of + * @param itemID ID of the item + */ public getNameVariant(listID: string, itemID: string): Observable { return this.appStore.pipe( select(relationshipStateSelector(listID, itemID)) ); } + /** + * Remove the name variant for item with ID "itemID" part of list with ID "listID" + * @param listID ID of the list the item is a part of + * @param itemID ID of the item + */ public removeNameVariant(listID: string, itemID: string) { this.appStore.dispatch(new RemoveNameVariantAction(listID, itemID)); } + /** + * Get the name variants of all items part of list with ID "listID" + * @param listID ID of the list the items are a part of + */ public getNameVariantsByListID(listID: string) { return this.appStore.pipe(select(relationshipListStateSelector(listID))); } + /** + * Get the relationship between two items with a name variant for the item on the opposite side of the relationship-label + * @param item1 Related item + * @param item2 Other related item + * @param relationshipLabel The label describing the relationship between the two items + * @param nameVariant The name variant to give the item on the opposite side of the relationship-label + */ public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index 8bca76f7d2..762d546705 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -187,14 +187,27 @@ export class RouteService { ); } + /** + * Add a parameter to the current route + * @param key The parameter name + * @param value The parameter value + */ public addParameter(key, value) { this.store.dispatch(new AddParameterAction(key, value)); } + /** + * Set a parameter in the current route (overriding the previous value) + * @param key The parameter name + * @param value The parameter value + */ public setParameter(key, value) { this.store.dispatch(new SetParameterAction(key, value)); } + /** + * Get the current query and route parameters and add them + */ public setCurrentRouteInfo() { combineLatest(this.getRouteParams(), this.route.queryParams) .pipe(take(1)) diff --git a/src/app/core/utilities/equatable.ts b/src/app/core/utilities/equatable.ts index 1029a295ba..ce327c5cc3 100644 --- a/src/app/core/utilities/equatable.ts +++ b/src/app/core/utilities/equatable.ts @@ -1,6 +1,12 @@ import { getExcludedFromEqualsFor, getFieldsForEquals } from './equals.decorators'; import { hasNoValue, hasValue } from '../../shared/empty.util'; +/** + * Compare two objects by comparing a given list of their properties + * @param object1 The first object + * @param object2 The second object + * @param fieldList A list of fields to compare the two objects by + */ function equalsByFields(object1, object2, fieldList): boolean { const unequalProperty = fieldList.find((key) => { if (object1[key] === object2[key]) { @@ -27,6 +33,9 @@ function equalsByFields(object1, object2, fieldList): boolean { return hasNoValue(unequalProperty); } +/** + * An object with a defined equals method + */ export abstract class EquatableObject { equals(other: T): boolean { if (hasNoValue(other)) { diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index 673370cc2c..c0512b4995 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -13,6 +13,9 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models'; styleUrls: ['./external-source-entry-list-submission-element.component.scss'], templateUrl: './external-source-entry-list-submission-element.component.html' }) +/** + * The component for displaying a list element of an external source entry + */ export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { /** * The metadata value for the object's uri diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts index 34eab47b47..3b5d8077cc 100644 --- a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -6,7 +6,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './name-variant-modal.component.html', styleUrls: ['./name-variant-modal.component.scss'] }) +/** + * The component for the modal to add a name variant to an item + */ export class NameVariantModalComponent { + /** + * The name variant + */ @Input() value: string; constructor(public modal: NgbActiveModal) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts index 173509acf9..cb4db189e1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts @@ -9,6 +9,9 @@ import { RelationshipTypeService } from '../../../../../../core/data/relationshi selector: 'ds-dynamic-disabled', templateUrl: './dynamic-disabled.component.html' }) +/** + * Component for displaying a form input with a disabled property + */ export class DsDynamicDisabledComponent extends DynamicFormControlComponent { @Input() formId: string; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index 39f3e12bb9..c03d025530 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -41,16 +41,53 @@ import { ExternalSourceService } from '../../../../../core/data/external-source. } ] }) - +/** + * Modal component for looking up relations + */ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ label: string; + + /** + * Options for searching related items + */ relationshipOptions: RelationshipOptions; + + /** + * The ID of the list to add/remove selected items to/from + */ listId: string; + + /** + * The item we're adding relationships to + */ item; + + /** + * Is the selection repeatable? + */ repeatable: boolean; + + /** + * The list of selected items + */ selection$: Observable; + + /** + * The context to display lists + */ context: Context; + + /** + * The metadata-fields describing these relationships + */ metadataFields: string; + + /** + * A map of subscriptions within this component + */ subMap: { [uuid: string]: Subscription } = {}; @@ -104,6 +141,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy this.modal.close(); } + /** + * Select (a list of) objects and add them to the store + * @param selectableObjects + */ select(...selectableObjects: Array>) { this.zone.runOutsideAngular( () => { @@ -131,6 +172,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy }); } + /** + * Add a subscription updating relationships with name variants + * @param sri The search result to track name variants for + */ private addNameVariantSubscription(sri: SearchResult) { const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid); this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe( @@ -138,6 +183,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant))) } + /** + * Deselect (a list of) objects and remove them from the store + * @param selectableObjects + */ deselect(...selectableObjects: Array>) { this.zone.runOutsideAngular( () => selectableObjects.forEach((object) => { @@ -147,6 +196,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ); } + /** + * Set existing name variants for items by the item's virtual metadata + */ private setExistingNameVariants() { const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index a8332dd3a1..d1fa538de3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -30,15 +30,43 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination fadeInOut ] }) - +/** + * The tab displaying a list of importable entries for an external source + */ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ @Input() label: string; + + /** + * The ID of the list to add/remove selected items to/from + */ @Input() listId: string; + + /** + * Is the selection repeatable? + */ @Input() repeatable: boolean; + + /** + * The context to display lists + */ @Input() context: Context; + + /** + * Send an event to deselect an object from the list + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * The initial pagination to start with + */ initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'submission-external-source-relation-list', pageSize: 5 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index fb2884cb37..1c4a8793b1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -33,25 +33,81 @@ import { LookupRelationService } from '../../../../../../core/data/lookup-relati } ] }) - +/** + * Tab for browsing local entities to add to the selection + */ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy { + /** + * Options for searching related items + */ @Input() relationship: RelationshipOptions; + + /** + * The ID of the list to add/remove selected items to/from + */ @Input() listId: string; + + /** + * Is the selection repeatable? + */ @Input() repeatable: boolean; + + /** + * The list of selected items + */ @Input() selection$: Observable; + + /** + * The context to display lists + */ @Input() context: Context; + /** + * Send an event to deselect an object from the list + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ @Output() selectObject: EventEmitter = new EventEmitter(); + + /** + * Search results + */ resultsRD$: Observable>>>; + + /** + * Are all results selected? + */ allSelected: boolean; + + /** + * Are some results selected? + */ someSelected$: Observable; + + /** + * Is it currently loading to select all results? + */ selectAllLoading: boolean; + + /** + * Subscription to unsubscribe from + */ subscription; + + /** + * The initial pagination to use + */ initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'submission-relation-list', pageSize: 5 }); + + /** + * The type of links to display + */ linkTypes = CollectionElementLinkType; constructor( @@ -76,12 +132,19 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest ); } + /** + * Reset the route parameters + */ resetRoute() { this.router.navigate([], { queryParams: Object.assign({}, { pageSize: this.initialPagination.pageSize }, this.route.snapshot.queryParams, { page: 1 }) }); } + /** + * Select all results within the page provided + * @param page + */ selectPage(page: Array>) { this.selection$ .pipe(take(1)) @@ -92,6 +155,10 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.selectableListService.select(this.listId, page); } + /** + * Deselect all results within the page provided + * @param page + */ deselectPage(page: Array>) { this.allSelected = false; this.selection$ @@ -103,6 +170,9 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.selectableListService.deselect(this.listId, page); } + /** + * Select all results + */ selectAll() { this.allSelected = true; this.selectAllLoading = true; @@ -128,6 +198,9 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest ); } + /** + * Deselect all + */ deselectAll() { this.allSelected = false; this.selection$ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index 3dcde9ea8e..076887b01e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -24,15 +24,48 @@ import { Context } from '../../../../../../core/shared/context.model'; } ] }) - +/** + * Tab displaying the currently selected relations to add + */ export class DsDynamicLookupRelationSelectionTabComponent { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ @Input() label: string; + + /** + * The ID of the list to add/remove selected items to/from + */ @Input() listId: string; + + /** + * Is the selection repeatable? + */ @Input() repeatable: boolean; + + /** + * The list of selected items + */ @Input() selection$: Observable; + + /** + * The paginated list of selected items + */ @Input() selectionRD$: Observable>>; + + /** + * The context to display lists + */ @Input() context: Context; + + /** + * Send an event to deselect an object from the list + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ @Output() selectObject: EventEmitter = new EventEmitter(); constructor(private router: Router, diff --git a/src/app/shared/form/builder/models/relationship-options.model.ts b/src/app/shared/form/builder/models/relationship-options.model.ts index 7d9542794b..57c6cbca2c 100644 --- a/src/app/shared/form/builder/models/relationship-options.model.ts +++ b/src/app/shared/form/builder/models/relationship-options.model.ts @@ -1,5 +1,8 @@ const RELATION_METADATA_PREFIX = 'relation.' +/** + * Extra options for displaying search results of relationships + */ export class RelationshipOptions { relationshipType: string; filter: string; From 890d60aa9c7995d0b75fceea02afb0734bef2df1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 19 Dec 2019 13:13:46 +0100 Subject: [PATCH 47/77] 67478: External source link + pagination fix --- ...al-source-entry-list-submission-element.component.html | 2 +- .../dynamic-lookup-relation-selection-tab.component.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html index 5595be6b7d..55b8f38a5e 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -1,2 +1,2 @@
        {{object.display}}
        - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index 7b4ad23208..f4746853f6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -69,6 +69,14 @@ export class DsDynamicLookupRelationSelectionTabComponent { */ @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * The initial pagination to use + */ + initialPagination = Object.assign(new PaginationComponentOptions(), { + id: 'submission-relation-list', + pageSize: 5 + }); + constructor(private router: Router, private searchConfigService: SearchConfigurationService) { } From ea05af74bd05a45ebfc794e2b396c32aa09d2b58 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 19 Dec 2019 17:34:13 +0100 Subject: [PATCH 48/77] 67611: Import external source entry window layout changes --- ...rnal-source-entry-import-modal.component.html | 16 +++++++++------- ...ternal-source-entry-import-modal.component.ts | 5 +++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html index b88fea8e41..a4fc356ef9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.html @@ -13,15 +13,17 @@

        {{ (labelPrefix + 'select' | translate) }}

        -
        -
        - - -
        -
        +
        {{ (labelPrefix + 'entities' | translate) }}
        +
        +
        + + +
        +
        + {{ (labelPrefix + 'entities.new' | translate) }}
        -
        +
        {{ (labelPrefix + 'authority' | translate) }}
        diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index 8ca8305085..7e0fe78717 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -142,6 +142,11 @@ export class ExternalSourceEntryImportModalComponent implements OnInit { */ importedObject: EventEmitter = new EventEmitter(); + /** + * Should it display the ability to import the entry as an authority? + */ + authorityEnabled = false; + constructor(public modal: NgbActiveModal, public lookupRelationService: LookupRelationService, private selectService: SelectableListService, From 9d185e8a152d46600960faa7e70105f159bddf9f Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 20 Dec 2019 08:55:23 +0100 Subject: [PATCH 49/77] fixing tests --- src/app/core/data/data.service.spec.ts | 2 +- src/app/core/data/relationship.service.spec.ts | 4 ++-- src/app/core/data/relationship.service.ts | 2 +- .../object-select/item-select/item-select.component.spec.ts | 2 +- .../search-label/search-label.component.spec.ts | 5 +++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 98e5f7afaa..ca5f2cc12e 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -192,7 +192,7 @@ describe('DataService', () => { dso2.self = selfLink; dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(service, 'findByHref').and.returnValues(createSuccessfulRemoteDataObject$(dso)); + spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); spyOn(objectCache, 'addPatch'); }); diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index b33db80fbe..9287935f59 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -123,8 +123,8 @@ describe('RelationshipService', () => { 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); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid); }); }); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index e155b1f90b..325bb59399 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -150,7 +150,7 @@ export class RelationshipService extends DataService { this.requestService.removeByHrefSubstring(item.uuid); combineLatest( this.objectCache.hasBySelfLinkObservable(item.self), - this.requestService.hasByHrefObservable(item.self) + this.requestService.hasByHrefObservable(item.uuid) ).pipe( filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), take(1), diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index 26dd55f010..08305080ca 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -16,7 +16,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { of } from 'rxjs/internal/observable/of'; -describe('ItemSelectComponent', () => { +fdescribe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; let objectSelectService: ObjectSelectService; diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts index 0b382015af..5de87be3bc 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts @@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Observable, of as observableOf } from 'rxjs'; -import { Params } from '@angular/router'; +import { Params, Router } from '@angular/router'; import { SearchLabelComponent } from './search-label.component'; import { ObjectKeysPipe } from '../../../utils/object-keys-pipe'; import { SEARCH_CONFIG_SERVICE } from '../../../../+my-dspace-page/my-dspace-page.component'; @@ -39,7 +39,8 @@ describe('SearchLabelComponent', () => { declarations: [SearchLabelComponent, ObjectKeysPipe], providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, - { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() } + { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, + { provide: Router, useValue: {}} // { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} } ], schemas: [NO_ERRORS_SCHEMA] From 65b648b00016a1a3bd76e515e8ec9465f6ee5ca7 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 20 Dec 2019 16:48:40 +0100 Subject: [PATCH 50/77] fixed existing tests --- ...ng-metadata-list-element.component.spec.ts | 12 +++- ...xisting-metadata-list-element.component.ts | 5 +- .../item-select/item-select.component.spec.ts | 2 +- .../submission-form-collection.component.ts | 59 ++++++++++++------- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index 7172653557..1b2f471f0b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -1,6 +1,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ExistingMetadataListElementComponent } from './existing-metadata-list-element.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { Store } from '@ngrx/store'; describe('ExistingMetadataListElementComponent', () => { let component: ExistingMetadataListElementComponent; @@ -8,9 +11,14 @@ describe('ExistingMetadataListElementComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ExistingMetadataListElementComponent ] + declarations: [ExistingMetadataListElementComponent], + providers: [ + { provide: SelectableListService, useValue: {} }, + { provide: Store, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index a0de289a04..d2b519b9a3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -15,11 +15,13 @@ import { Store } from '@ngrx/store'; import { AppState } from '../../../../../app.reducer'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +// tslint:disable:max-classes-per-file export abstract class Reorderable { constructor(public oldIndex?: number, public newIndex?: number) { } abstract getId(): string; + abstract getPlace(): number; } @@ -84,7 +86,7 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); if (hasValue(relationMD)) { const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); - this.metadataRepresentation = Object.assign( + this.metadataRepresentation = Object.assign( new ItemMetadataRepresentation(metadataRepresentationMD), this.relatedItem ) @@ -107,3 +109,4 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro } } +// tslint:enable:max-classes-per-file diff --git a/src/app/shared/object-select/item-select/item-select.component.spec.ts b/src/app/shared/object-select/item-select/item-select.component.spec.ts index 08305080ca..26dd55f010 100644 --- a/src/app/shared/object-select/item-select/item-select.component.spec.ts +++ b/src/app/shared/object-select/item-select/item-select.component.spec.ts @@ -16,7 +16,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { of } from 'rxjs/internal/observable/of'; -fdescribe('ItemSelectComponent', () => { +describe('ItemSelectComponent', () => { let comp: ItemSelectComponent; let fixture: ComponentFixture; let objectSelectService: ObjectSelectService; 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 88bc4904d3..f84764d6a4 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -1,8 +1,28 @@ -import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; import { FormControl } from '@angular/forms'; import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter, map, mergeMap, reduce, startWith, flatMap, find } from 'rxjs/operators'; +import { + debounceTime, + distinctUntilChanged, + filter, + find, + flatMap, + map, + mergeMap, + reduce, + startWith +} from 'rxjs/operators'; import { Collection } from '../../../core/shared/collection.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; @@ -197,23 +217,21 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { find((communities: RemoteData>) => isNotEmpty(communities.payload)), mergeMap((communities: RemoteData>) => communities.payload.page)); - const listCollection$ = observableOf([]); - - // const listCollection$ = communities$.pipe( - // flatMap((communityData: Community) => { - // return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( - // find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), - // mergeMap((collections: RemoteData>) => collections.payload.page), - // filter((collectionData: Collection) => isNotEmpty(collectionData)), - // map((collectionData: Collection) => ({ - // communities: [{ id: communityData.id, name: communityData.name }], - // collection: { id: collectionData.id, name: collectionData.name } - // })) - // ); - // }), - // reduce((acc: any, value: any) => [...acc, ...value], []), - // startWith([]) - // ); + const listCollection$ = communities$.pipe( + flatMap((communityData: Community) => { + return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( + find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), + mergeMap((collections: RemoteData>) => collections.payload.page), + filter((collectionData: Collection) => isNotEmpty(collectionData)), + map((collectionData: Collection) => ({ + communities: [{ id: communityData.id, name: communityData.name }], + collection: { id: collectionData.id, name: collectionData.name } + })) + ); + }), + reduce((acc: any, value: any) => [...acc, ...value], []), + startWith([]) + ); const searchTerm$ = this.searchField.valueChanges.pipe( debounceTime(200), @@ -229,8 +247,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { } else { return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); } - }) - ); + })); } } } From 4dd66073fcb46bac527a2dee9d48b22c36ba3665 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 23 Dec 2019 11:19:23 +0100 Subject: [PATCH 51/77] added tests for new component --- ...ng-metadata-list-element.component.spec.ts | 67 +++++++++++++++++-- ...xisting-metadata-list-element.component.ts | 1 - 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index 1b2f471f0b..fa13febcd1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -1,20 +1,59 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ExistingMetadataListElementComponent } from './existing-metadata-list-element.component'; +import { ExistingMetadataListElementComponent, Reorderable, ReorderableRelationship } from './existing-metadata-list-element.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; describe('ExistingMetadataListElementComponent', () => { let component: ExistingMetadataListElementComponent; let fixture: ComponentFixture; + let selectionService; + let store; + let listID; + let submissionItem; + let relationship; + let reoRel; + let metadataFields; + let relationshipOptions; + let uuid1; + let uuid2; + let relatedItem; + let leftItemRD$; + let rightItemRD$; + let relatedSearchResult; + + function init() { + uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000'; + uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7'; + selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']); + store = jasmine.createSpyObj('store', ['dispatch']); + listID = '1234-listID'; + submissionItem = Object.assign(new Item(), { uuid: uuid1 }); + metadataFields = ['dc.contributor.author']; + relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true }) + relatedItem = Object.assign(new Item(), { uuid: uuid2 }); + leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem); + rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem); + relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem }); + + relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ }); + reoRel = new ReorderableRelationship(relationship, true); + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ declarations: [ExistingMetadataListElementComponent], providers: [ - { provide: SelectableListService, useValue: {} }, - { provide: Store, useValue: {} }, + { provide: SelectableListService, useValue: selectionService }, + { provide: Store, useValue: store }, ], schemas: [NO_ERRORS_SCHEMA] }) @@ -24,10 +63,30 @@ describe('ExistingMetadataListElementComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ExistingMetadataListElementComponent); component = fixture.componentInstance; + component.listId = listID; + component.submissionItem = submissionItem; + component.reoRel = reoRel; + component.metadataFields = metadataFields; + component.relationshipOptions = relationshipOptions; fixture.detectChanges(); + component.ngOnChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('removeSelection', () => { + it('should deselect the object in the selectable list service', () => { + component.removeSelection(); + expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult); + }); + + it('should dispatch a RemoveRelationshipAction', () => { + component.removeSelection(); + const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType); + expect(store.dispatch).toHaveBeenCalledWith(action); + + }); + }) }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index d2b519b9a3..3c2c5dbe8b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -76,7 +76,6 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro ngOnChanges() { const item$ = this.reoRel.useLeftItem ? this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; - this.subs.push(item$.pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), From ce004c2e58b7123ea27115ab8efbc88e621e420f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 23 Dec 2019 15:10:08 +0100 Subject: [PATCH 52/77] Resolve conflict occurred when two or more pagination components are present in the same page --- .../shared/pagination/pagination.component.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 014f01f152..9c378d1aff 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -225,10 +225,14 @@ export class PaginationComponent implements OnDestroy, OnInit { } /** + * @param cdRef + * ChangeDetectorRef is a singleton service provided by Angular. * @param route * Route is a singleton service provided by Angular. * @param router * Router is a singleton service provided by Angular. + * @param hostWindowService + * the HostWindowService singleton. */ constructor(private cdRef: ChangeDetectorRef, private route: ActivatedRoute, @@ -243,7 +247,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page being navigated to. */ public doPageChange(page: number) { - this.updateRoute({ page: page.toString() }); + this.updateRoute({ pageId: this.id, page: page.toString() }); } /** @@ -253,7 +257,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page size being navigated to. */ public doPageSizeChange(pageSize: number) { - this.updateRoute({ page: 1, pageSize: pageSize }); + this.updateRoute({ pageId: this.id, page: 1, pageSize: pageSize }); } /** @@ -263,7 +267,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort direction being navigated to. */ public doSortDirectionChange(sortDirection: SortDirection) { - this.updateRoute({ page: 1, sortDirection: sortDirection }); + this.updateRoute({ pageId: this.id, page: 1, sortDirection: sortDirection }); } /** @@ -273,7 +277,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort field being navigated to. */ public doSortFieldChange(field: string) { - this.updateRoute({ page: 1, sortField: field }); + this.updateRoute({ pageId: this.id, page: 1, sortField: field }); } /** @@ -413,27 +417,30 @@ export class PaginationComponent implements OnDestroy, OnInit { * Method to update all pagination variables to the current query parameters */ private setFields() { - // (+) converts string to a number - const page = this.currentQueryParams.page; - if (this.currentPage !== +page) { - this.setPage(+page); - } + // set fields only when page id is the one configured for this pagination instance + if (this.currentQueryParams.pageId === this.id) { + // (+) converts string to a number + const page = this.currentQueryParams.page; + if (this.currentPage !== +page) { + this.setPage(+page); + } - const pageSize = this.currentQueryParams.pageSize; - if (this.pageSize !== +pageSize) { - this.setPageSize(+pageSize); - } + const pageSize = this.currentQueryParams.pageSize; + if (this.pageSize !== +pageSize) { + this.setPageSize(+pageSize); + } - const sortDirection = this.currentQueryParams.sortDirection; - if (this.sortDirection !== sortDirection) { - this.setSortDirection(sortDirection); - } + const sortDirection = this.currentQueryParams.sortDirection; + if (this.sortDirection !== sortDirection) { + this.setSortDirection(sortDirection); + } - const sortField = this.currentQueryParams.sortField; - if (this.sortField !== sortField) { - this.setSortField(sortField); + const sortField = this.currentQueryParams.sortField; + if (this.sortField !== sortField) { + this.setSortField(sortField); + } + this.cdRef.detectChanges(); } - this.cdRef.detectChanges(); } /** From e004098016d86a06b75fe5b75a793870a66bf3ec Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 23 Dec 2019 15:11:38 +0100 Subject: [PATCH 53/77] Added pagination to community's collection list in the community page --- ...ty-page-sub-collection-list.component.html | 15 +- ...page-sub-collection-list.component.spec.ts | 169 ++++++++++++++++++ ...nity-page-sub-collection-list.component.ts | 63 ++++++- 3 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index 9156a99b18..bf6ce7fd57 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,14 +1,13 @@

        {{'community.sub-collection-list.head' | translate}}

        - + +
        diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts new file mode 100644 index 0000000000..2150676dd6 --- /dev/null +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,169 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; +import { Community } from '../../core/shared/community.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { FindListOptions } from '../../core/data/request.models'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; + +describe('CommunityPageSubCollectionListComponent Component', () => { + let comp: CommunityPageSubCollectionListComponent; + let fixture: ComponentFixture; + let collectionDataServiceStub: any; + let subCollList = []; + + const collections = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 3' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-4', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 7' } + ] + } + }) + ]; + + const mockCommunity = Object.assign(new Community(), { + id: '123456789', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Test title' } + ] + } + }); + + collectionDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCollList.length) { + endPageIndex = subCollList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, + RouterTestingModule.withRoutes([]), + NoopAnimationsModule], + declarations: [CommunityPageSubCollectionListComponent], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent); + comp = fixture.componentInstance; + comp.community = mockCommunity; + }); + + it('should display a list of collections', () => { + subCollList = collections; + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); + }); + + it('should not display the header when collection list is empty', () => { + subCollList = []; + fixture.detectChanges(); + + const subComHead = fixture.debugElement.queryAll(By.css('h2')); + expect(subComHead.length).toEqual(0); + }); + + it('should update list of collection on pagination change', () => { + subCollList = collections; + fixture.detectChanges(); + + const pagination = Object.create({}); + pagination.pageId = comp.pageId; + pagination.page = 2; + pagination.pageSize = 5; + pagination.sortField = 'dc.title'; + pagination.sortDirection = 'ASC'; + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('Collection 6'); + expect(collList[1].nativeElement.textContent).toContain('Collection 7'); + }); +}); diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index b8a5d60002..18f48c3482 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,12 +1,16 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../core/data/collection-data.service'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list'; }) export class CommunityPageSubCollectionListComponent implements OnInit { @Input() community: Community; - subCollectionsRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-collections-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CollectionDataService) {} ngOnInit(): void { - this.subCollectionsRDObs = this.community.collections; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.page; + this.config.pageSize = event.pageSize; + this.sortConfig.field = event.sortField; + this.sortConfig.direction = event.sortDirection; + this.updatePage(); + } + + /** + * Update the list of collections + */ + updatePage() { + this.cds.findByParent(this.community.id,{ + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCollectionsRDObs.next(results); + }); } } From fcbb690b29cf87bf421826d4b4c6e8f0abc1181e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 23 Dec 2019 15:12:05 +0100 Subject: [PATCH 54/77] Added pagination to community's sub-community list in the community page --- ...ity-page-sub-community-list.component.html | 15 +- ...-page-sub-community-list.component.spec.ts | 138 ++++++++++++++---- ...unity-page-sub-community-list.component.ts | 66 ++++++++- 3 files changed, 178 insertions(+), 41 deletions(-) diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html index 6cd62ba48d..880ea9cc8e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html @@ -1,14 +1,13 @@

        {{'community.sub-community-list.head' | translate}}

        - + +
        diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 2feaa3afa6..ff3eb4d074 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,21 +1,26 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NO_ERRORS_SCHEMA} from '@angular/core'; -import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component'; -import {Community} from '../../core/shared/community.model'; -import {RemoteData} from '../../core/data/remote-data'; -import {PaginatedList} from '../../core/data/paginated-list'; -import {PageInfo} from '../../core/shared/page-info.model'; -import {SharedModule} from '../../shared/shared.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {By} from '@angular/platform-browser'; -import {of as observableOf, Observable } from 'rxjs'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; describe('SubCommunityList Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; + let communityDataServiceStub: any; + let subCommList = []; const subcommunities = [Object.assign(new Community(), { id: '123456789-1', @@ -32,27 +37,76 @@ describe('SubCommunityList Component', () => { { language: 'en_US', value: 'SubCommunity 2' } ] } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' } + ] + } }) ]; - const emptySubCommunitiesCommunity = Object.assign(new Community(), { + const mockCommunity = Object.assign(new Community(), { + id: '123456789', metadata: { 'dc.title': [ { language: 'en_US', value: 'Test title' } ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } }); - const mockCommunity = Object.assign(new Community(), { - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) - }) - ; + communityDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCommList.length) { + endPageIndex = subCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -60,6 +114,10 @@ describe('SubCommunityList Component', () => { RouterTestingModule.withRoutes([]), NoopAnimationsModule], declarations: [CommunityPageSubCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -67,23 +125,47 @@ describe('SubCommunityList Component', () => { beforeEach(() => { fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); comp = fixture.componentInstance; + comp.community = mockCommunity; + }); it('should display a list of subCommunities', () => { - comp.community = mockCommunity; + subCommList = subcommunities; fixture.detectChanges(); const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(2); + expect(subComList.length).toEqual(5); expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); it('should not display the header when subCommunities are empty', () => { - comp.community = emptySubCommunitiesCommunity; + subCommList = []; fixture.detectChanges(); const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); + + it('should update list of collection on pagination change', () => { + subCommList = subcommunities; + fixture.detectChanges(); + + const pagination = Object.create({}); + pagination.pageId = comp.pageId; + pagination.page = 2; + pagination.pageSize = 5; + pagination.sortField = 'dc.title'; + pagination.sortDirection = 'ASC'; + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7'); + }); }); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 91f6d7bac1..34ef05c2c4 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,26 +1,82 @@ import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; -import {Observable} from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; @Component({ selector: 'ds-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', - animations:[fadeIn] + animations: [fadeIn] }) /** * Component to render the sub-communities of a Community */ export class CommunityPageSubCommunityListComponent implements OnInit { @Input() community: Community; - subCommunitiesRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-subCommunities-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CommunityDataService) { + } ngOnInit(): void { - this.subCommunitiesRDObs = this.community.subcommunities; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.page; + this.config.pageSize = event.pageSize; + this.sortConfig.field = event.sortField; + this.sortConfig.direction = event.sortDirection; + this.updatePage(); + } + + /** + * Update the list of sub-communities + */ + updatePage() { + this.cds.findByParent(this.community.id, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCommunitiesRDObs.next(results); + }); } } From b18cfcbd25c9ab332fef4cc5205d56a3c9f99665 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 23 Dec 2019 17:23:17 +0100 Subject: [PATCH 55/77] fixed issue with type of object emitted onPaginationChange event --- ...page-sub-collection-list.component.spec.ts | 35 ++++++++++++------ ...nity-page-sub-collection-list.component.ts | 8 ++-- ...-page-sub-community-list.component.spec.ts | 37 +++++++++++++------ ...unity-page-sub-community-list.component.ts | 8 ++-- .../top-level-community-list.component.ts | 23 +++++++----- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 2150676dd6..09332dda16 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -5,6 +5,8 @@ import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; import { Community } from '../../core/shared/community.model'; import { SharedModule } from '../../shared/shared.module'; @@ -15,8 +17,9 @@ import { PaginatedList } from '../../core/data/paginated-list'; import { PageInfo } from '../../core/shared/page-info.model'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; -describe('CommunityPageSubCollectionListComponent Component', () => { +describe('CommunityPageSubCollectionList Component', () => { let comp: CommunityPageSubCollectionListComponent; let fixture: ComponentFixture; let collectionDataServiceStub: any; @@ -109,13 +112,18 @@ describe('CommunityPageSubCollectionListComponent Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, + imports: [ + TranslateModule.forRoot(), + SharedModule, RouterTestingModule.withRoutes([]), - NoopAnimationsModule], + NgbModule.forRoot(), + NoopAnimationsModule + ], declarations: [CommunityPageSubCollectionListComponent], providers: [ { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -140,7 +148,7 @@ describe('CommunityPageSubCollectionListComponent Component', () => { expect(collList[4].nativeElement.textContent).toContain('Collection 5'); }); - it('should not display the header when collection list is empty', () => { + it('should not display the header when list of collections is empty', () => { subCollList = []; fixture.detectChanges(); @@ -148,16 +156,21 @@ describe('CommunityPageSubCollectionListComponent Component', () => { expect(subComHead.length).toEqual(0); }); - it('should update list of collection on pagination change', () => { + it('should update list of collections on pagination change', () => { subCollList = collections; fixture.detectChanges(); - const pagination = Object.create({}); - pagination.pageId = comp.pageId; - pagination.page = 2; - pagination.pageSize = 5; - pagination.sortField = 'dc.title'; - pagination.sortDirection = 'ASC'; + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); comp.onPaginationChange(pagination); fixture.detectChanges(); diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index 18f48c3482..64c274444e 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -57,10 +57,10 @@ export class CommunityPageSubCollectionListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index ff3eb4d074..41502e7bd4 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -5,6 +5,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; import { Community } from '../../core/shared/community.model'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -15,8 +17,9 @@ import { FindListOptions } from '../../core/data/request.models'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; -describe('SubCommunityList Component', () => { +describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; let communityDataServiceStub: any; @@ -110,13 +113,18 @@ describe('SubCommunityList Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, + imports: [ + TranslateModule.forRoot(), + SharedModule, RouterTestingModule.withRoutes([]), - NoopAnimationsModule], + NgbModule.forRoot(), + NoopAnimationsModule + ], declarations: [CommunityPageSubCommunityListComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -129,7 +137,7 @@ describe('SubCommunityList Component', () => { }); - it('should display a list of subCommunities', () => { + it('should display a list of sub-communities', () => { subCommList = subcommunities; fixture.detectChanges(); @@ -142,7 +150,7 @@ describe('SubCommunityList Component', () => { expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); - it('should not display the header when subCommunities are empty', () => { + it('should not display the header when list of sub-communities is empty', () => { subCommList = []; fixture.detectChanges(); @@ -150,16 +158,21 @@ describe('SubCommunityList Component', () => { expect(subComHead.length).toEqual(0); }); - it('should update list of collection on pagination change', () => { + it('should update list of sub-communities on pagination change', () => { subCommList = subcommunities; fixture.detectChanges(); - const pagination = Object.create({}); - pagination.pageId = comp.pageId; - pagination.page = 2; - pagination.pageSize = 5; - pagination.sortField = 'dc.title'; - pagination.sortDirection = 'ASC'; + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); comp.onPaginationChange(pagination); fixture.detectChanges(); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 34ef05c2c4..1bd664021e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -60,10 +60,10 @@ export class CommunityPageSubCommunityListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 1115d785a3..02c3cb54a0 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; - import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { take } from 'rxjs/operators'; /** * this component renders the Top-Level Community list @@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit { */ config: PaginationComponentOptions; + /** + * The pagination id + */ + pageId = 'top-level-pagination'; + /** * The sorting configuration */ @@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit { constructor(private cds: CommunityDataService) { this.config = new PaginationComponentOptions(); - this.config.id = 'top-level-pagination'; + this.config.id = this.pageId; this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); @@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } From c09010e15142a6e213993265c67ec83627d7568a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 23 Dec 2019 17:23:43 +0100 Subject: [PATCH 56/77] Added test for TopLevelCommunityList Component --- ...top-level-community-list.component.spec.ts | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts new file mode 100644 index 0000000000..fa164fe624 --- /dev/null +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -0,0 +1,161 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TopLevelCommunityListComponent } from './top-level-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('TopLevelCommunityList Component', () => { + let comp: TopLevelCommunityListComponent; + let fixture: ComponentFixture; + let communityDataServiceStub: any; + + const topCommList = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 7' } + ] + } + }) + ]; + + communityDataServiceStub = { + findTop(options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > topCommList.length) { + endPageIndex = topCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [TopLevelCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopLevelCommunityListComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + + }); + + it('should display a list of top-communities', () => { + const subComList = fixture.debugElement.queryAll(By.css('li')); + + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); + }); + + it('should update list of top-communities on pagination change', () => { + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7'); + }); +}); From dee06c69a6e912324debdcb5c8fa4cf30983de33 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 6 Jan 2020 12:57:00 +0100 Subject: [PATCH 57/77] 64503: Collection harvester settings refresh cache after submitting changes --- .../collection-source.component.spec.ts | 9 +++++++-- .../collection-source.component.ts | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index a44311a1c7..11ec9b1f6a 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -20,6 +20,7 @@ import { By } from '@angular/platform-browser'; import { Collection } from '../../../core/shared/collection.model'; import { RemoteData } from '../../../core/data/remote-data'; import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { RequestService } from '../../../core/data/request.service'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -36,6 +37,7 @@ let formService: DynamicFormService; let router: any; let collection: Collection; let collectionService: CollectionDataService; +let requestService: RequestService; describe('CollectionSourceComponent', () => { let comp: CollectionSourceComponent; @@ -111,8 +113,10 @@ describe('CollectionSourceComponent', () => { }); collectionService = jasmine.createSpyObj('collectionService', { getContentSource: observableOf(contentSource), - updateContentSource: observableOf(contentSource) + updateContentSource: observableOf(contentSource), + getHarvesterEndpoint: observableOf('harvester-endpoint') }); + requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], @@ -125,7 +129,8 @@ describe('CollectionSourceComponent', () => { { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: new RemoteData(false, false, true, null, collection) }) } } }, { provide: Router, useValue: router }, { provide: GLOBAL_CONFIG, useValue: { collection: { edit: { undoTimeout: 10 } } } as any }, - { provide: CollectionDataService, useValue: collectionService } + { provide: CollectionDataService, useValue: collectionService }, + { provide: RequestService, useValue: requestService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts index 8ae8efdfe5..5fcc740663 100644 --- a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -31,6 +31,7 @@ import { CollectionDataService } from '../../../core/data/collection-data.servic import { getSucceededRemoteData } from '../../../core/shared/operators'; import { MetadataConfig } from '../../../core/shared/metadata-config.model'; import { INotification } from '../../../shared/notifications/models/notification.model'; +import { RequestService } from '../../../core/data/request.service'; /** * Component for managing the content source of the collection @@ -236,7 +237,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem protected route: ActivatedRoute, protected router: Router, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected collectionService: CollectionDataService) { + protected collectionService: CollectionDataService, + protected requestService: RequestService) { super(objectUpdatesService, notificationsService, translate); } @@ -373,6 +375,15 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Submit the edited Content Source to the REST API, re-initialize the field update and display a notification */ onSubmit() { + // Remove cached harvester request to allow for latest harvester to be displayed when switching tabs + this.collectionRD$.pipe( + getSucceededRemoteData(), + map((col) => col.payload.uuid), + switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)), + take(1) + ).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint)); + + // Update harvester this.collectionRD$.pipe( getSucceededRemoteData(), map((col) => col.payload.uuid), From b1575b3336398c3af8aff282d05388348e084393 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 6 Jan 2020 16:08:43 +0100 Subject: [PATCH 58/77] 67611: Remove redundant test --- .../dynamic-lookup-relation-selection-tab.component.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts index 203a4df0b0..18e5d3c3ab 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts @@ -73,11 +73,6 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => { expect(component).toBeTruthy(); }); - it('should call navigate on the router when is called resetRoute', () => { - component.resetRoute(); - expect(router.navigate).toHaveBeenCalled(); - }); - it('should call navigate on the router when is called resetRoute', () => { component.selectionRD$ = createSelection([]); fixture.detectChanges(); From bb87bf5a0e25e59566f1c00afc6cb9f76f859fa4 Mon Sep 17 00:00:00 2001 From: Ben Bosman Date: Tue, 7 Jan 2020 18:26:26 +0100 Subject: [PATCH 59/77] display the entity type in myDSpace --- .../item-list-preview.component.html | 1 + .../item-list-preview.component.spec.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index bcd5c3c027..e1478b5206 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -2,6 +2,7 @@ +

        diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts index eb531d2f93..6366539157 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts @@ -47,6 +47,23 @@ const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), { ] } }); +const mockItemWithEntityType: Item = Object.assign(new Item(), { + bundles: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'relationship.type': [ + { + language: null, + value: 'Publication' + } + ] + } +}); describe('ItemListPreviewComponent', () => { beforeEach(async(() => { @@ -128,4 +145,16 @@ describe('ItemListPreviewComponent', () => { expect(dateField).not.toBeNull(); }); }); + + describe('When the item has an entity type', () => { + beforeEach(() => { + component.item = mockItemWithEntityType; + fixture.detectChanges(); + }); + + it('should show the entity type span', () => { + const entityField = fixture.debugElement.query(By.css('ds-item-type-badge')); + expect(entityField).not.toBeNull(); + }); + }); }); From f9fa8f0347826fd3cde72588643d51990e91cbc7 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 8 Jan 2020 10:34:24 +0100 Subject: [PATCH 60/77] added typedoc --- src/app/core/data/relationship.service.ts | 5 +++++ .../ds-dynamic-form-control-container.component.ts | 5 +++++ .../existing-metadata-list-element.component.ts | 12 ++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 325bb59399..35ddd700eb 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -390,6 +390,11 @@ export class RelationshipService extends DataService { return update$ } + /** + * Method to update the the right or left place of a relationship + * The useLeftItem field in the reorderable relationship determines which place should be updated + * @param reoRel + */ public updatePlace(reoRel: ReorderableRelationship): Observable> { let updatedRelationship; if (reoRel.useLeftItem) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index c8903b19ee..01b13e60da 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -343,6 +343,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.item = this.item; } + /** + * Method to move a relationship inside the list of relationships + * This will update the view and update the right or left place field of the relationships in the list + * @param event + */ moveSelection(event: CdkDragDrop) { this.zone.runOutsideAngular(() => { moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index 3c2c5dbe8b..09aaa253c6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -16,6 +16,9 @@ import { AppState } from '../../../../../app.reducer'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; // tslint:disable:max-classes-per-file +/** + * Abstract class that defines objects that can be reordered + */ export abstract class Reorderable { constructor(public oldIndex?: number, public newIndex?: number) { } @@ -25,6 +28,9 @@ export abstract class Reorderable { abstract getPlace(): number; } +/** + * Represents a single relationship that can be reordered in a list of multiple relationships + */ export class ReorderableRelationship extends Reorderable { relationship: Relationship; useLeftItem: boolean; @@ -48,6 +54,9 @@ export class ReorderableRelationship extends Reorderable { } } +/** + * Represents a single existing relationship value as metadata in submission + */ @Component({ selector: 'ds-existing-metadata-list-element', templateUrl: './existing-metadata-list-element.component.html', @@ -93,6 +102,9 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro })); } + /** + * Removes the selected relationship from the list + */ removeSelection() { this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem })); this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType)) From feced9f8939c3c48b47065d8ac7029de1697a7e8 Mon Sep 17 00:00:00 2001 From: Ben Bosman Date: Wed, 8 Jan 2020 17:41:29 +0100 Subject: [PATCH 61/77] submit to this collection --- .../collection-page.component.html | 7 +++++++ .../core/submission/submission-rest.service.ts | 17 +++++++++++++---- src/app/submission/submission.service.spec.ts | 8 ++++++++ src/app/submission/submission.service.ts | 6 ++++-- .../submit/submission-submit.component.spec.ts | 4 +++- .../submit/submission-submit.component.ts | 17 ++++++++++++++--- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 12d5c200fd..11f406599a 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -32,6 +32,13 @@ + + +
        { service.createSubmission(); expect((service as any).restService.postToEndpoint).toHaveBeenCalled(); + expect((service as any).restService.postToEndpoint).toHaveBeenCalledWith('workspaceitems', {}, null, null, undefined); + }); + + it('should create a new submission with collection', () => { + service.createSubmission(collectionId); + + expect((service as any).restService.postToEndpoint).toHaveBeenCalled(); + expect((service as any).restService.postToEndpoint).toHaveBeenCalledWith('workspaceitems', {}, null, null, collectionId); }); }); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index fa8024af53..1991e8b3f1 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -109,11 +109,13 @@ export class SubmissionService { /** * Perform a REST call to create a new workspaceitem and return response * + * @param collectionId + * The owning collection id * @return Observable * observable of SubmissionObject */ - createSubmission(): Observable { - return this.restService.postToEndpoint(this.workspaceLinkPath, {}).pipe( + createSubmission(collectionId?: string): Observable { + return this.restService.postToEndpoint(this.workspaceLinkPath, {}, null, null, collectionId).pipe( map((workspaceitem: SubmissionObject) => workspaceitem[0]), catchError(() => observableOf({}))) } diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index 771171a2d1..ca3316669f 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -1,6 +1,6 @@ import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core'; import { of as observableOf } from 'rxjs'; @@ -14,6 +14,7 @@ import { getMockTranslateService } from '../../shared/mocks/mock-translate.servi import { RouterStub } from '../../shared/testing/router-stub'; import { mockSubmissionObject } from '../../shared/mocks/mock-submission'; import { SubmissionSubmitComponent } from './submission-submit.component'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; describe('SubmissionSubmitComponent Component', () => { @@ -39,6 +40,7 @@ describe('SubmissionSubmitComponent Component', () => { { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: TranslateService, useValue: getMockTranslateService() }, { provide: Router, useValue: new RouterStub() }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, ViewContainerRef ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 448ccf97e2..0aa0380a25 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewContainerRef } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; @@ -27,6 +27,12 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { */ public collectionId: string; + /** + * The collection id input to create a new submission + * @type {string} + */ + public collectionParam: string; + /** * The submission self url * @type {string} @@ -60,13 +66,18 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * @param {Router} router * @param {TranslateService} translate * @param {ViewContainerRef} viewContainerRef + * @param {ActivatedRoute} route */ constructor(private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, private router: Router, private submissionService: SubmissionService, private translate: TranslateService, - private viewContainerRef: ViewContainerRef) { + private viewContainerRef: ViewContainerRef, + private route: ActivatedRoute) { + this.route + .queryParams + .subscribe((params) => { this.collectionParam = (params.collection); }); } /** @@ -75,7 +86,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { ngOnInit() { // NOTE execute the code on the browser side only, otherwise it is executed twice this.subs.push( - this.submissionService.createSubmission() + this.submissionService.createSubmission(this.collectionParam) .subscribe((submissionObject: SubmissionObject) => { // NOTE new submission is created on the browser side only if (isNotNull(submissionObject)) { From 34fb8612f8871e2c88376d0f4db681b768cb93ab Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 5 Nov 2019 16:34:53 +0100 Subject: [PATCH 62/77] Expandable search form in header with tests (& e2e tests) Squashed: 66021: Expandable search form in header 66021: Adjustment small screens 66021: search icon now pressable and css partially moved to bootstrap classes 66021: start testing of header search box; TODO fix e2e 66021: E2E Tests 66021: Header search box now with angular animation and bootstrap > css; e2e fix 66021: feedback 2019-12-19 style changes 66021: Fix navbar search tests Patch: add opacity to header search animation Change the input style 66021: expandable search navbar - tests 66021: Expandable search form in header Small fix after rebasing upstream/master --- e2e/search-navbar/search-navbar.e2e-spec.ts | 46 +++++++ e2e/search-navbar/search-navbar.po.ts | 40 ++++++ src/app/app.module.ts | 34 ++--- .../community-list-adapter.ts | 0 src/app/header/header.component.html | 34 ++--- .../search-navbar.component.html | 12 ++ .../search-navbar.component.scss | 25 ++++ .../search-navbar.component.spec.ts | 121 ++++++++++++++++++ .../search-navbar/search-navbar.component.ts | 71 ++++++++++ src/app/shared/animations/slide.ts | 38 ++++-- 10 files changed, 378 insertions(+), 43 deletions(-) create mode 100644 e2e/search-navbar/search-navbar.e2e-spec.ts create mode 100644 e2e/search-navbar/search-navbar.po.ts delete mode 100644 src/app/community-list-page/community-list-adapter.ts create mode 100644 src/app/search-navbar/search-navbar.component.html create mode 100644 src/app/search-navbar/search-navbar.component.scss create mode 100644 src/app/search-navbar/search-navbar.component.spec.ts create mode 100644 src/app/search-navbar/search-navbar.component.ts diff --git a/e2e/search-navbar/search-navbar.e2e-spec.ts b/e2e/search-navbar/search-navbar.e2e-spec.ts new file mode 100644 index 0000000000..b60f71919d --- /dev/null +++ b/e2e/search-navbar/search-navbar.e2e-spec.ts @@ -0,0 +1,46 @@ +import { ProtractorPage } from './search-navbar.po'; +import { browser } from 'protractor'; + +describe('protractor SearchNavbar', () => { + let page: ProtractorPage; + let queryString: string; + + beforeEach(() => { + page = new ProtractorPage(); + queryString = 'the test query'; + }); + + it('should go to search page with correct query if submitted (from home)', () => { + page.navigateToHome(); + return checkIfSearchWorks(); + }); + + it('should go to search page with correct query if submitted (from search)', () => { + page.navigateToSearch(); + return checkIfSearchWorks(); + }); + + it('check if can submit search box with pressing button', () => { + page.navigateToHome(); + page.expandAndFocusSearchBox(); + page.setCurrentQuery(queryString); + page.submitNavbarSearchForm(); + browser.wait(() => { + return browser.getCurrentUrl().then((url: string) => { + return url.indexOf('query=' + encodeURI(queryString)) !== -1; + }); + }); + }); + + function checkIfSearchWorks(): boolean { + page.setCurrentQuery(queryString); + page.submitByPressingEnter(); + browser.wait(() => { + return browser.getCurrentUrl().then((url: string) => { + return url.indexOf('query=' + encodeURI(queryString)) !== -1; + }); + }); + return false; + } + +}); diff --git a/e2e/search-navbar/search-navbar.po.ts b/e2e/search-navbar/search-navbar.po.ts new file mode 100644 index 0000000000..17112ab468 --- /dev/null +++ b/e2e/search-navbar/search-navbar.po.ts @@ -0,0 +1,40 @@ +import { browser, element, by, protractor } from 'protractor'; +import { promise } from 'selenium-webdriver'; + +export class ProtractorPage { + HOME = '/home'; + SEARCH = '/search'; + + navigateToHome() { + return browser.get(this.HOME); + } + + navigateToSearch() { + return browser.get(this.SEARCH); + } + + getCurrentQuery(): promise.Promise { + return element(by.css('#search-navbar-container form input')).getAttribute('value'); + } + + expandAndFocusSearchBox() { + element(by.css('#search-navbar-container form a')).click(); + } + + setCurrentQuery(query: string) { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(query); + } + + submitNavbarSearchForm() { + element(by.css('#search-navbar-container form .submit-icon')).click(); + } + + submitByPressingEnter() { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER); + } + + submitByPressingEnter() { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER); + } + +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 926575d711..4b803608f3 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,10 +10,14 @@ import { META_REDUCERS, MetaReducer, StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { TranslateModule } from '@ngx-translate/core'; +import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { storeFreeze } from 'ngrx-store-freeze'; import { ENV_CONFIG, GLOBAL_CONFIG, GlobalConfig } from '../config'; +import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; +import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component'; +import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -23,23 +27,20 @@ import { appMetaReducers, debugMetaReducers } from './app.metareducers'; import { appReducers, AppState } from './app.reducer'; import { CoreModule } from './core/core.module'; -import { FooterComponent } from './footer/footer.component'; -import { HeaderComponent } from './header/header.component'; -import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; - -import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer'; -import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; -import { NotificationComponent } from './shared/notifications/notification/notification.component'; -import { SharedModule } from './shared/shared.module'; -import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; -import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component'; -import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component'; -import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; -import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; -import { NavbarModule } from './navbar/navbar.module'; import { ClientCookieService } from './core/services/client-cookie.service'; import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module'; +import { FooterComponent } from './footer/footer.component'; +import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component'; +import { HeaderComponent } from './header/header.component'; +import { NavbarModule } from './navbar/navbar.module'; +import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; +import { SearchNavbarComponent } from './search-navbar/search-navbar.component'; + +import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer'; +import { NotificationComponent } from './shared/notifications/notification/notification.component'; +import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; +import { SharedModule } from './shared/shared.module'; export function getConfig() { return ENV_CONFIG; @@ -112,7 +113,8 @@ const DECLARATIONS = [ FooterComponent, PageNotFoundComponent, NotificationComponent, - NotificationsBoardComponent + NotificationsBoardComponent, + SearchNavbarComponent, ]; const EXPORTS = [ @@ -128,7 +130,7 @@ const EXPORTS = [ ...PROVIDERS ], declarations: [ - ...DECLARATIONS + ...DECLARATIONS, ], exports: [ ...EXPORTS diff --git a/src/app/community-list-page/community-list-adapter.ts b/src/app/community-list-page/community-list-adapter.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index a03fd01c53..58f7cb1ecf 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,20 +1,20 @@
        -
        - - - +
        + + + - -
        + +
        diff --git a/src/app/search-navbar/search-navbar.component.html b/src/app/search-navbar/search-navbar.component.html new file mode 100644 index 0000000000..13d792c80f --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.html @@ -0,0 +1,12 @@ +
        +
        +
        + + + + +
        +
        +
        diff --git a/src/app/search-navbar/search-navbar.component.scss b/src/app/search-navbar/search-navbar.component.scss new file mode 100644 index 0000000000..3606c47afc --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.scss @@ -0,0 +1,25 @@ +input[type="text"] { + margin-top: -0.5 * $font-size-base; + + &:focus { + background-color: rgba(255, 255, 255, 0.5) !important; + } + + &.collapsed { + opacity: 0; + } +} + +a.submit-icon { + cursor: pointer; +} + + + +@media screen and (max-width: map-get($grid-breakpoints, sm)) { + #query:focus { + max-width: 250px !important; + width: 40vw !important; + } +} + diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts new file mode 100644 index 0000000000..2a03acd2a2 --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -0,0 +1,121 @@ +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { SearchService } from '../core/shared/search/search.service'; +import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; + +import { SearchNavbarComponent } from './search-navbar.component'; + +describe('SearchNavbarComponent', () => { + let component: SearchNavbarComponent; + let fixture: ComponentFixture; + let mockSearchService: any; + let router: Router; + let routerStub; + + beforeEach(async(() => { + mockSearchService = { + getSearchLink() { + return '/search'; + } + }; + + routerStub = { + navigate: (commands) => commands + }; + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + declarations: [SearchNavbarComponent], + providers: [ + { provide: SearchService, useValue: mockSearchService }, + { provide: Router, useValue: routerStub } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchNavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + router = (component as any).router; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when you click on search icon', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'expand').and.callThrough(); + spyOn(component, 'onSubmit').and.callThrough(); + spyOn(router, 'navigate').and.callThrough(); + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + + it('input expands', () => { + expect(component.expand).toHaveBeenCalled(); + }); + + describe('empty query', () => { + describe('press submit button', () => { + beforeEach(fakeAsync(() => { + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('to search page with empty query', () => { + expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + }); + + describe('fill in some query', () => { + let searchInput; + beforeEach(async () => { + await fixture.whenStable(); + fixture.detectChanges(); + searchInput = fixture.debugElement.query(By.css('#search-navbar-container form input')); + searchInput.nativeElement.value = 'test'; + searchInput.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + }); + describe('press submit button', () => { + beforeEach(fakeAsync(() => { + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', null); + tick(); + fixture.detectChanges(); + })); + it('to search page with query', async () => { + expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + }) + + }); +}); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts new file mode 100644 index 0000000000..1bedfb73ef --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.ts @@ -0,0 +1,71 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { SearchService } from '../core/shared/search/search.service'; +import { expandSearchInput } from '../shared/animations/slide'; + +/** + * The search box in the header that expands on focus and collapses on focus out + */ +@Component({ + selector: 'ds-search-navbar', + templateUrl: './search-navbar.component.html', + styleUrls: ['./search-navbar.component.scss'], + animations: [expandSearchInput] +}) +export class SearchNavbarComponent { + + // The search form + searchForm; + // Whether or not the search bar is expanded, boolean for html ngIf, string fo AngularAnimation state change + searchExpanded = false; + isExpanded = 'collapsed'; + + // Search input field + @ViewChild('searchInput') searchField: ElementRef; + + constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + /** + * Expands search bar by angular animation, see expandSearchInput + */ + expand() { + this.searchExpanded = true; + this.isExpanded = 'expanded'; + this.editSearch(); + } + + /** + * Collapses & blurs search bar by angular animation, see expandSearchInput + */ + collapse() { + this.searchField.nativeElement.blur(); + this.searchExpanded = false; + this.isExpanded = 'collapsed'; + } + + /** + * Focuses on input search bar so search can be edited + */ + editSearch(): void { + this.searchField.nativeElement.focus(); + } + + /** + * Submits the search (on enter or on search icon click) + * @param data Data for the searchForm, containing the search query + */ + onSubmit(data: any) { + this.collapse(); + const linkToNavigateTo = this.searchService.getSearchLink().split('/'); + this.searchForm.reset(); + this.router.navigate(linkToNavigateTo, { + queryParams: Object.assign({}, { page: 1 }, data), + queryParamsHandling: 'merge' + }); + } +} diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts index 38bfaaddca..7928a25659 100644 --- a/src/app/shared/animations/slide.ts +++ b/src/app/shared/animations/slide.ts @@ -1,13 +1,4 @@ -import { - animate, - animateChild, - group, - query, - state, - style, - transition, - trigger -} from '@angular/animations'; +import { animate, animateChild, group, query, state, style, transition, trigger } from '@angular/animations'; export const slide = trigger('slide', [ state('expanded', style({ height: '*' })), @@ -70,3 +61,30 @@ export const slideSidebarPadding = trigger('slideSidebarPadding', [ transition('hidden <=> expanded', [animate('200ms')]), transition('shown <=> expanded', [animate('200ms')]), ]); + +export const expandSearchInput = trigger('toggleAnimation', [ + state('collapsed', style({ + width: '30px', + opacity: '0' + })), + state('expanded', style({ + width: '250px', + opacity: '1' + })), + transition('* => collapsed', group([ + animate('300ms ease-in-out', style({ + width: '30px' + })), + animate('300ms ease-in', style({ + opacity: '0' + })) + ])), + transition('* => expanded', group([ + animate('300ms ease-out', style({ + opacity: '1' + })), + animate('300ms ease-in-out', style({ + width: '250px' + })) + ])) +]); From c57810f00d2d3cfd7aa880c6a2473d38850bf823 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 10 Jan 2020 15:04:52 +0100 Subject: [PATCH 63/77] Timeout time for e2e tests --- protractor.conf.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protractor.conf.js b/protractor.conf.js index 2949702a0a..6570c9f7c3 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -5,7 +5,7 @@ var SpecReporter = require('jasmine-spec-reporter').SpecReporter; exports.config = { - allScriptsTimeout: 11000, + allScriptsTimeout: 600000, // ----------------------------------------------------------------- // Uncomment to run tests using a remote Selenium server //seleniumAddress: 'http://selenium.address:4444/wd/hub', @@ -73,7 +73,7 @@ exports.config = { framework: 'jasmine', jasmineNodeOpts: { showColors: true, - defaultTimeoutInterval: 30000, + defaultTimeoutInterval: 600000, print: function () {} }, useAllAngular2AppRoots: true, From d3d2bb20d44af727c6a606f9cc4d684f8b00330e Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 10 Jan 2020 15:28:57 +0100 Subject: [PATCH 64/77] id css selector added to remove ambiguity in search-page e2e tests --- e2e/search-page/search-page.po.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/search-page/search-page.po.ts b/e2e/search-page/search-page.po.ts index fde3e68bf8..51bf86453b 100644 --- a/e2e/search-page/search-page.po.ts +++ b/e2e/search-page/search-page.po.ts @@ -1,4 +1,4 @@ -import { browser, element, by, protractor } from 'protractor'; +import { browser, by, element, protractor } from 'protractor'; import { promise } from 'selenium-webdriver'; export class ProtractorPage { @@ -27,15 +27,15 @@ export class ProtractorPage { } setCurrentScope(scope: string) { - element(by.css('option[value="' + scope + '"]')).click(); + element(by.css('#search-form option[value="' + scope + '"]')).click(); } setCurrentQuery(query: string) { - element(by.css('input[name="query"]')).sendKeys(query); + element(by.css('#search-form input[name="query"]')).sendKeys(query); } submitSearchForm() { - element(by.css('button.search-button')).click(); + element(by.css('#search-form button.search-button')).click(); } getRandomScopeOption(): promise.Promise { From 3a37f9be12059c142fca1e5167eccfe1c11a707b Mon Sep 17 00:00:00 2001 From: Ben Bosman Date: Fri, 10 Jan 2020 18:15:28 +0100 Subject: [PATCH 65/77] submit to this collection --- .../admin-sidebar/admin-sidebar.component.ts | 21 ++++++++++--------- .../collection-page.component.html | 7 ------- .../submission/submission-rest.service.ts | 2 +- ...ate-item-parent-selector.component.spec.ts | 5 ++--- .../create-item-parent-selector.component.ts | 9 ++++++-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 185d083764..72eb306bf1 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -17,6 +17,7 @@ import { CreateCollectionParentSelectorComponent } from '../../shared/dso-select import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import {CreateItemParentSelectorComponent} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; /** * Component representing the admin sidebar @@ -137,18 +138,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { parentID: 'new', active: false, visible: true, - // model: { - // type: MenuItemType.ONCLICK, - // text: 'menu.section.new_item', - // function: () => { - // this.modalService.open(CreateItemParentSelectorComponent); - // } - // } as OnClickMenuItemModel, model: { - type: MenuItemType.LINK, + type: MenuItemType.ONCLICK, text: 'menu.section.new_item', - link: '/submit' - } as LinkMenuItemModel, + function: () => { + this.modalService.open(CreateItemParentSelectorComponent); + } + } as OnClickMenuItemModel, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.new_item', + // link: '/submit' + // } as LinkMenuItemModel, }, { id: 'new_item_version', diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 11f406599a..12d5c200fd 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -32,13 +32,6 @@ - - -
        { }); it('should call navigate on the router with the correct create path when navigate is called', () => { - /* TODO when there is a specific submission path */ - // component.navigate(item); - // expect(router.navigate).toHaveBeenCalledWith([createPath]); + component.navigate(collection); + expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } }); }); }); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 29af9f624e..02a0bd79cd 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -28,6 +28,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo * Navigate to the item create page */ navigate(dso: DSpaceObject) { - // There's no submit path per collection yet... + const navigationExtras: NavigationExtras = { + queryParams: { + ['collection']: dso.uuid, + } + }; + this.router.navigate(['/submit'], navigationExtras); } } From 617bbbb90c9c869fcf6d06adbf03f8027555fe89 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 14 Jan 2020 12:52:50 +0100 Subject: [PATCH 66/77] 67478: Remove redundant test --- .../dynamic-lookup-relation-selection-tab.component.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts index 203a4df0b0..18e5d3c3ab 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts @@ -73,11 +73,6 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => { expect(component).toBeTruthy(); }); - it('should call navigate on the router when is called resetRoute', () => { - component.resetRoute(); - expect(router.navigate).toHaveBeenCalled(); - }); - it('should call navigate on the router when is called resetRoute', () => { component.selectionRD$ = createSelection([]); fixture.detectChanges(); From 9d71f0348bbeb70e8e682dbe5ab478c866d8e822 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 14 Jan 2020 13:38:01 +0100 Subject: [PATCH 67/77] 67611: fix test --- src/app/core/data/item-data.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index efc82de955..8263601e28 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -211,7 +211,7 @@ describe('ItemDataService', () => { }); it('should configure a POST request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest), undefined)); + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); }); }); From 1bc718237247afd3732c52af85c182211031f299 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 14 Jan 2020 14:04:42 +0100 Subject: [PATCH 68/77] fixes after merge --- src/app/core/data/data.service.ts | 2 +- src/app/core/data/relationship.service.ts | 4 ++-- .../ds-dynamic-form-control-container.component.ts | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index c8dac45e75..d55b7353eb 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -252,7 +252,7 @@ export abstract class DataService { return oldVersion$.pipe( getSucceededRemoteData(), getRemoteDataPayload(), - mergeMap((oldVersion: NormalizedObject) => { + mergeMap((oldVersion: T) => { const operations = this.comparator.diff(oldVersion, object); if (isNotEmpty(operations)) { this.objectCache.addPatch(object.self, operations); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index c262de6370..d6993ebcee 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs'; +import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { AppState, keySelector } from '../../app.reducer'; import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; @@ -356,7 +356,7 @@ export class RelationshipService extends DataService { * @param nameVariant The name variant to set for the matching relationship */ public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { - const update$ = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) + const update$: Observable> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( switchMap((relation: Relationship) => relation.relationshipType.pipe( diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index f7ca14574d..22376502e7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -75,7 +75,7 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; -import { map, startWith, switchMap, take } from 'rxjs/operators'; +import { map, startWith, switchMap, find } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; @@ -353,20 +353,21 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo moveSelection(event: CdkDragDrop) { this.zone.runOutsideAngular(() => { moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); - const reorderables = this.reorderables.map((reo: Reorderable, index: number) => { + const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => { reo.oldIndex = reo.getPlace(); reo.newIndex = index; return reo; } ); - return observableCombineLatest(reorderables.map((rel: ReorderableRelationship) => { + observableCombineLatest( + reorderables.map((rel: ReorderableRelationship) => { if (rel.oldIndex !== rel.newIndex) { return this.relationshipService.updatePlace(rel); } else { - return observableOf(undefined); + return observableOf(undefined) as Observable>; } }) - ).pipe(getSucceededRemoteData()).subscribe(); + ).subscribe(); }) } From 78f9d62e004217d6c333f66e47a0a15a664a695f Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 14 Jan 2020 14:42:14 +0100 Subject: [PATCH 69/77] fixed linting error --- src/app/shared/sidebar/filter/sidebar-filter.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts index 60fc40d0a8..b67de24f9e 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.service.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.service.ts @@ -16,7 +16,7 @@ import { hasValue } from '../../empty.util'; @Injectable() export class SidebarFilterService { - constructor(private store:Store) { + constructor(private store: Store) { } /** From 027182130544c0c76c995e84f0cf42a5e6c83e5b Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 14 Jan 2020 14:50:46 +0100 Subject: [PATCH 70/77] updated tslint rules that have been renamed --- tslint.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tslint.json b/tslint.json index b4f905d324..92bb66cee1 100644 --- a/tslint.json +++ b/tslint.json @@ -40,7 +40,7 @@ ], "no-access-missing-member": false, "no-arg": true, - "no-attribute-parameter-decorator": true, + "no-attribute-decorator": true, "no-bitwise": true, "no-console": [ true, @@ -81,9 +81,8 @@ false ], "prefer-const": true, - "pipe-naming": [ + "pipe-prefix": [ true, - "camelCase", "ds" ], "quotemark": [ @@ -147,10 +146,10 @@ "no-input-rename": true, "no-output-rename": true, "templates-use-public": false, - "use-host-property-decorator": true, - "use-input-property-decorator": true, + "no-host-metadata-property": true, + "no-inputs-metadata-property": true, "use-life-cycle-interface": false, - "use-output-property-decorator": true, + "no-outputs-metadata-property": true, "use-pipe-transform-interface": true // "rxjs-collapse-imports": true, // "rxjs-pipeable-operators-only": true, From 2e06357291fecb9453891f5e4b03ec46cb76f8ab Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 15 Jan 2020 11:12:49 +0100 Subject: [PATCH 71/77] upgrade to bionic --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 901dee8186..4dd4943f7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: required -dist: trusty +dist: bionic env: # Install the latest docker-compose version for ci testing. @@ -38,7 +38,6 @@ addons: sources: - google-chrome packages: - - dpkg - google-chrome-stable language: node_js From 1d6e9d5b96b7807cfbacd2f0f92acf6b18b2cb90 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 15 Jan 2020 11:32:29 +0100 Subject: [PATCH 72/77] add xvfb service for headless chrome --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4dd4943f7c..9c80cc0447 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ env: DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_SSL: false +services: + - xvfb + before_install: # Docker Compose Install - curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose From 94a71b69d1022a0932f346e4216fe8f6db4faf5b Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 15 Jan 2020 11:56:04 +0100 Subject: [PATCH 73/77] remove chrome source, doesn't work anyway and doesn't seem to be needed. Remove chromium to try to solve the issue where travis can't find chrome --- .travis.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c80cc0447..c42923886d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,13 +36,6 @@ before_script: after_script: - docker-compose -f ./docker/docker-compose-travis.yml down -addons: - apt: - sources: - - google-chrome - packages: - - google-chrome-stable - language: node_js node_js: @@ -55,8 +48,6 @@ cache: bundler_args: --retry 5 script: - # Use Chromium instead of Chrome. - - export CHROME_BIN=chromium-browser - yarn run build - yarn run ci - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js From 4e8ec5f4a2aef80642ebb04a6b36cc723dd9359f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Jan 2020 12:10:58 +0100 Subject: [PATCH 74/77] Fix history and bitstreamFormats store selector after moved in CoreState --- src/app/core/data/bitstream-format-data.service.ts | 6 +++++- src/app/core/history/selectors.ts | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index b5c2b708dc..c30330a0a3 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -25,8 +25,12 @@ import { import { hasValue } from '../../shared/empty.util'; import { RequestEntry } from './request.reducer'; import { CoreState } from '../core.reducers'; +import { coreSelector } from '../core.selectors'; -const bitstreamFormatsStateSelector = (state: CoreState) => state.bitstreamFormats; +const bitstreamFormatsStateSelector = createSelector( + coreSelector, + (state: CoreState) => state.bitstreamFormats +); const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector, (bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats); diff --git a/src/app/core/history/selectors.ts b/src/app/core/history/selectors.ts index a04d3839b1..5c77cd65f0 100644 --- a/src/app/core/history/selectors.ts +++ b/src/app/core/history/selectors.ts @@ -1,3 +1,8 @@ import { CoreState } from '../core.reducers'; +import { createSelector } from '@ngrx/store'; +import { coreSelector } from '../core.selectors'; -export const historySelector = (state: CoreState) => state.history; +export const historySelector = createSelector( + coreSelector, + (state: CoreState) => state.history +); From 3fd4ecbfc46a4ce4a1afda861f576bab51a34070 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 16 Jan 2020 14:47:37 +0100 Subject: [PATCH 75/77] 67611: ExpressionChangedAfterItHasBeenCheckedError fix --- src/app/app.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1f3da086c2..2141b06a77 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { filter, map, take } from 'rxjs/operators'; +import { delay, filter, map, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; @@ -125,8 +125,11 @@ export class AppComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - this.router.events - .subscribe((event) => { + this.router.events.pipe( + // This fixes an ExpressionChangedAfterItHasBeenCheckedError from being thrown while loading the component + // More information on this bug-fix: https://blog.angular-university.io/angular-debugging/ + delay(0) + ).subscribe((event) => { if (event instanceof NavigationStart) { this.isLoading = true; } else if ( From ddb787277b6a7b0f8fb8192b3e76f21d1d5b242b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 17 Jan 2020 13:08:26 +0100 Subject: [PATCH 76/77] Fix failed test --- src/app/core/services/route.service.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/core/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts index 525329d50f..07ff56d879 100644 --- a/src/app/core/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -142,7 +142,11 @@ describe('RouteService', () => { describe('getHistory', () => { it('should dispatch AddUrlToHistoryAction on NavigationEnd event', () => { - serviceAsAny.store = observableOf({ history: ['url', 'newurl'] }); + serviceAsAny.store = observableOf({ + core: { + history: ['url', 'newurl'] + } + }); service.getHistory().subscribe((history) => { expect(history).toEqual(['url', 'newurl']); From a403732d45c0e4121302bd6d47ca88faf6e01e33 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 20 Jan 2020 16:52:53 +0100 Subject: [PATCH 77/77] 68276: Fixed configuration-search-page fixedFilterQuery parameter --- src/app/+search-page/configuration-search-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+search-page/configuration-search-page.component.ts b/src/app/+search-page/configuration-search-page.component.ts index 2cde216c05..befac7f331 100644 --- a/src/app/+search-page/configuration-search-page.component.ts +++ b/src/app/+search-page/configuration-search-page.component.ts @@ -62,7 +62,7 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements this.routeService.setParameter('configuration', this.configuration); } if (hasValue(this.fixedFilterQuery)) { - this.routeService.setParameter('fixedFilter', this.fixedFilterQuery); + this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery); } } }