From 8325a34910aea6be0de0e4b61ba8a6df3a5fe17c Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 4 Aug 2020 14:15:25 +0200 Subject: [PATCH 01/78] 72403: Support for process output on detail page --- src/app/core/core.module.ts | 4 + .../core/data/process-output-data.service.ts | 73 +++++++++++++++++++ .../shared/process-output.resource-type.ts | 9 +++ .../detail/process-detail.component.html | 13 +++- .../detail/process-detail.component.spec.ts | 53 +++++++++++++- .../detail/process-detail.component.ts | 24 +++++- src/app/process-page/process-page.resolver.ts | 2 +- .../processes/process-output.model.ts | 36 +++++++++ .../process-page/processes/process.model.ts | 9 +++ src/assets/i18n/en.json5 | 2 +- 10 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 src/app/core/data/process-output-data.service.ts create mode 100644 src/app/core/shared/process-output.resource-type.ts create mode 100644 src/app/process-page/processes/process-output.model.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5aa462d5e0..d262bfd0d6 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -6,6 +6,7 @@ import { EffectsModule } from '@ngrx/effects'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard'; +import { ProcessOutput } from '../process-page/processes/process-output.model'; import { isNotEmpty } from '../shared/empty.util'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; @@ -70,6 +71,7 @@ import { LookupRelationService } from './data/lookup-relation.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; +import { ProcessOutputDataService } from './data/process-output-data.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipService } from './data/relationship.service'; import { ResourcePolicyService } from './resource-policy/resource-policy.service'; @@ -281,6 +283,7 @@ const PROVIDERS = [ ItemTypeDataService, WorkflowActionDataService, ProcessDataService, + ProcessOutputDataService, ScriptDataService, ProcessFilesResponseParsingService, FeatureDataService, @@ -347,6 +350,7 @@ export const models = ExternalSourceEntry, Script, Process, + ProcessOutput, Version, VersionHistory, WorkflowAction, diff --git a/src/app/core/data/process-output-data.service.ts b/src/app/core/data/process-output-data.service.ts new file mode 100644 index 0000000000..28adbcd6ca --- /dev/null +++ b/src/app/core/data/process-output-data.service.ts @@ -0,0 +1,73 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { ProcessOutput } from '../../process-page/processes/process-output.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { PROCESS_OUTPUT_TYPE } from '../shared/process-output.resource-type'; +import { DataService } from './data.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; + +/* tslint:disable:max-classes-per-file */ +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = 'processes'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +// @ts-ignore +/** + * A service to retrieve output from processes from the REST API. + */ +@Injectable() +@dataService(PROCESS_OUTPUT_TYPE) +export class ProcessOutputDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link ProcessOutput}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link ProcessOutput} + * @param href The url of {@link ProcessOutput} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/shared/process-output.resource-type.ts b/src/app/core/shared/process-output.resource-type.ts new file mode 100644 index 0000000000..2e707d0bda --- /dev/null +++ b/src/app/core/shared/process-output.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from './resource-type'; + +/** + * The resource type for ProcessOutput + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const PROCESS_OUTPUT_TYPE = new ResourceType('processOutput'); diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 9cb1f1e6af..078c79a0d5 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -34,9 +34,16 @@
{{ process.processStatus }}
- - - + +
+      
+        {{ (outputLogs$ | async)?.join('\n\t') }}
+      
+    
+

+ {{ 'process.detail.logs.none' | translate }} +

+
{{'process.detail.back' | translate}} diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index dff481fdc6..c1efd233e8 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -1,5 +1,7 @@ +import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; +import { ProcessOutput } from '../processes/process-output.model'; import { ProcessDetailComponent } from './process-detail.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; @@ -21,13 +23,20 @@ describe('ProcessDetailComponent', () => { let fixture: ComponentFixture; let processService: ProcessDataService; + let processOutputService: ProcessOutputDataService; let nameService: DSONameService; let process: Process; let fileName: string; let files: Bitstream[]; + let processOutput; + function init() { + processOutput = Object.assign(new ProcessOutput(), { + logs: ['Process started', 'Process completed'] + } + ); process = Object.assign(new Process(), { processId: 1, scriptName: 'script-name', @@ -40,7 +49,15 @@ describe('ProcessDetailComponent', () => { name: '-i', value: 'identifier' } - ] + ], + _links: { + self: { + href: 'https://rest.api/processes/1' + }, + output: { + href: 'https://rest.api/processes/1/output' + } + } }); fileName = 'fake-file-name'; files = [ @@ -62,6 +79,9 @@ describe('ProcessDetailComponent', () => { processService = jasmine.createSpyObj('processService', { getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)) }); + processOutputService = jasmine.createSpyObj('processOutputService', { + findByHref: createSuccessfulRemoteDataObject$(processOutput) + }); nameService = jasmine.createSpyObj('nameService', { getName: fileName }); @@ -75,6 +95,7 @@ describe('ProcessDetailComponent', () => { providers: [ { provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } }, { provide: ProcessDataService, useValue: processService }, + { provide: ProcessOutputDataService, useValue: processOutputService }, { provide: DSONameService, useValue: nameService } ], schemas: [NO_ERRORS_SCHEMA] @@ -104,4 +125,32 @@ describe('ProcessDetailComponent', () => { expect(processFiles.textContent).toContain(fileName); }); + it('should display the process\'s output logs', () => { + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')).nativeElement; + expect(outputProcess.textContent).toContain('Process started'); + }); + + describe('if process has no output logs (yet)', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + const emptyProcessOutput = Object.assign(new ProcessOutput(), { + logs: [] + }); + spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); + fixture = TestBed.createComponent(ProcessDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }) + ); + it('should not display the process\'s output logs', () => { + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess).toBeNull(); + }); + it('should display message saying there are no output logs', () => { + const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement; + expect(noOutputProcess).toBeDefined(); + }); + } + ) + }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index b0e2c7e378..74027a4e72 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,9 +1,12 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs/internal/Observable'; +import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { RemoteData } from '../../core/data/remote-data'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -36,9 +39,15 @@ export class ProcessDetailComponent implements OnInit { */ filesRD$: Observable>>; + /** + * The Process's Output logs + */ + outputLogs$: Observable; + constructor(protected route: ActivatedRoute, protected router: Router, protected processService: ProcessDataService, + protected processOutputService: ProcessOutputDataService, protected nameService: DSONameService) { } @@ -56,6 +65,17 @@ export class ProcessDetailComponent implements OnInit { getFirstSucceededRemoteDataPayload(), switchMap((process: Process) => this.processService.getFiles(process.processId)) ); + + const processOutputRD$: Observable> = this.processRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) + ); + this.outputLogs$ = processOutputRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((processOutput: ProcessOutput) => { + return [processOutput.logs]; + }) + ) } /** @@ -63,7 +83,7 @@ export class ProcessDetailComponent implements OnInit { * @param bitstream */ getFileName(bitstream: Bitstream) { - return this.nameService.getName(bitstream); + return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown'; } } diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index 84821a2574..57c749e1cb 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -24,7 +24,7 @@ export class ProcessPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.processService.findById(route.params.id, followLink('script')).pipe( + return this.processService.findById(route.params.id, followLink('script'), followLink('output') ).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); } diff --git a/src/app/process-page/processes/process-output.model.ts b/src/app/process-page/processes/process-output.model.ts new file mode 100644 index 0000000000..4ae1731d26 --- /dev/null +++ b/src/app/process-page/processes/process-output.model.ts @@ -0,0 +1,36 @@ +import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type'; +import { CacheableObject } from '../../core/cache/object-cache.reducer'; +import { HALLink } from '../../core/shared/hal-link.model'; +import { autoserialize, deserialize } from 'cerialize'; +import { excludeFromEquals } from '../../core/utilities/equals.decorators'; +import { ResourceType } from '../../core/shared/resource-type'; +import { typedObject } from '../../core/cache/builders/build-decorators'; + +/** + * Object representing a process output object + */ +@typedObject +export class ProcessOutput implements CacheableObject { + static type = PROCESS_OUTPUT_TYPE; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The output strings for this ProcessOutput + */ + @autoserialize + logs: string[]; + + /** + * The {@link HALLink}s for this ProcessOutput + */ + @deserialize + _links: { + self: HALLink, + }; +} diff --git a/src/app/process-page/processes/process.model.ts b/src/app/process-page/processes/process.model.ts index 85de5337e7..891acb626d 100644 --- a/src/app/process-page/processes/process.model.ts +++ b/src/app/process-page/processes/process.model.ts @@ -1,3 +1,5 @@ +import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type'; +import { ProcessOutput } from './process-output.model'; import { ProcessStatus } from './process-status.model'; import { ProcessParameter } from './process-parameter.model'; import { CacheableObject } from '../../core/cache/object-cache.reducer'; @@ -85,4 +87,11 @@ export class Process implements CacheableObject { */ @link(SCRIPT) script?: Observable>; + + /** + * The output logs created by this Process + * Will be undefined unless the output {@link HALLink} has been resolved. + */ + @link(PROCESS_OUTPUT_TYPE) + output?: Observable>; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bfa1c81aa6..69e7df151e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,7 +2085,7 @@ "process.detail.output" : "Process Output", - "process.detail.output.alert" : "Work in progress - Process output is not available yet", + "process.detail.logs.none": "This process has no output logs (yet)", "process.detail.output-files" : "Output Files", From f2a381643028d4027b03df6f9123eda50aa2a321 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 4 Aug 2020 14:57:57 +0200 Subject: [PATCH 02/78] 72403: tab removed process output, message change & redundant switchmap removed --- src/app/process-page/detail/process-detail.component.html | 7 ++----- src/app/process-page/detail/process-detail.component.ts | 6 +++--- src/assets/i18n/en.json5 | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 078c79a0d5..e76d24d17a 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -35,11 +35,8 @@ -
-      
-        {{ (outputLogs$ | async)?.join('\n\t') }}
-      
-    
+
{{ (outputLogs$ | async)?.join('\n') }}

{{ 'process.detail.logs.none' | translate }}

diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index 74027a4e72..af6e3238a6 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -6,7 +6,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -72,8 +72,8 @@ export class ProcessDetailComponent implements OnInit { ); this.outputLogs$ = processOutputRD$.pipe( getFirstSucceededRemoteDataPayload(), - switchMap((processOutput: ProcessOutput) => { - return [processOutput.logs]; + map((processOutput: ProcessOutput) => { + return processOutput.logs; }) ) } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 69e7df151e..75db3eceb1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,7 +2085,7 @@ "process.detail.output" : "Process Output", - "process.detail.logs.none": "This process has no output logs (yet)", + "process.detail.logs.none": "This process has no output yet", "process.detail.output-files" : "Output Files", From 02c693363b6f5ed28202898a2cc9c5c21c269f51 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 13 Aug 2020 12:31:42 +0200 Subject: [PATCH 03/78] Unneeded ts-ignore removed --- src/app/core/data/process-output-data.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/data/process-output-data.service.ts b/src/app/core/data/process-output-data.service.ts index 28adbcd6ca..24f33a85b7 100644 --- a/src/app/core/data/process-output-data.service.ts +++ b/src/app/core/data/process-output-data.service.ts @@ -36,7 +36,6 @@ class DataServiceImpl extends DataService { } } -// @ts-ignore /** * A service to retrieve output from processes from the REST API. */ From f82e6fa48bad7214b53f424bbf8b313152ae5ee9 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 2 Sep 2020 19:35:35 +0200 Subject: [PATCH 04/78] 72403: Process output logs only retrieved at button press + tests --- .../detail/process-detail.component.html | 22 ++++--- .../detail/process-detail.component.spec.ts | 66 +++++++++++++------ .../detail/process-detail.component.ts | 51 ++++++++++---- src/app/process-page/process-page.resolver.ts | 2 +- src/assets/i18n/en.json5 | 4 ++ 5 files changed, 103 insertions(+), 42 deletions(-) diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index e76d24d17a..e13770a0a3 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -34,13 +34,19 @@
{{ process.processStatus }}
- -
{{ (outputLogs$ | async)?.join('\n') }}
-

- {{ 'process.detail.logs.none' | translate }} -

-
+ + + +
{{ (outputLogs$ | async)?.join('\n') }}
+

+ {{ 'process.detail.logs.none' | translate }} +

+
- {{'process.detail.back' | translate}} + diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts index c1efd233e8..8d4a3f0e62 100644 --- a/src/app/process-page/detail/process-detail.component.spec.ts +++ b/src/app/process-page/detail/process-detail.component.spec.ts @@ -1,7 +1,7 @@ import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { ProcessOutput } from '../processes/process-output.model'; import { ProcessDetailComponent } from './process-detail.component'; -import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { VarDirective } from '../../shared/utils/var.directive'; import { TranslateModule } from '@ngx-translate/core'; import { RouterTestingModule } from '@angular/router/testing'; @@ -93,7 +93,10 @@ describe('ProcessDetailComponent', () => { declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ - { provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } }, + { + provide: ActivatedRoute, + useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } + }, { provide: ProcessDataService, useValue: processService }, { provide: ProcessOutputDataService, useValue: processOutputService }, { provide: DSONameService, useValue: nameService } @@ -105,15 +108,16 @@ describe('ProcessDetailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProcessDetailComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should display the script\'s name', () => { + fixture.detectChanges(); const name = fixture.debugElement.query(By.css('#process-name')).nativeElement; expect(name.textContent).toContain(process.scriptName); }); it('should display the process\'s parameters', () => { + fixture.detectChanges(); const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement; process.parameters.forEach((param) => { expect(args.textContent).toContain(`${param.name} ${param.value}`) @@ -121,27 +125,52 @@ describe('ProcessDetailComponent', () => { }); it('should display the process\'s output files', () => { + fixture.detectChanges(); const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement; expect(processFiles.textContent).toContain(fileName); }); - it('should display the process\'s output logs', () => { - const outputProcess = fixture.debugElement.query(By.css('#process-output pre')).nativeElement; - expect(outputProcess.textContent).toContain('Process started'); + describe('if press show output logs', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + })); + it('should trigger showProcessOutputLogs', () => { + expect(component.showProcessOutputLogs).toHaveBeenCalled(); + }); + it('should display the process\'s output logs', () => { + fixture.detectChanges(); + const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); + expect(outputProcess.nativeElement.textContent).toContain('Process started'); + }); }); - describe('if process has no output logs (yet)', () => { + describe('if press show output logs and process has no output logs (yet)', () => { beforeEach(fakeAsync(() => { - jasmine.getEnv().allowRespy(true); - const emptyProcessOutput = Object.assign(new ProcessOutput(), { - logs: [] - }); - spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); - fixture = TestBed.createComponent(ProcessDetailComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }) - ); + jasmine.getEnv().allowRespy(true); + const emptyProcessOutput = Object.assign(new ProcessOutput(), { + logs: [] + }); + spyOn(processOutputService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(emptyProcessOutput)); + fixture = TestBed.createComponent(ProcessDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + spyOn(component, 'showProcessOutputLogs').and.callThrough(); + fixture.detectChanges(); + const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton')); + showOutputButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); it('should not display the process\'s output logs', () => { const outputProcess = fixture.debugElement.query(By.css('#process-output pre')); expect(outputProcess).toBeNull(); @@ -150,7 +179,6 @@ describe('ProcessDetailComponent', () => { const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement; expect(noOutputProcess).toBeDefined(); }); - } - ) + }); }); diff --git a/src/app/process-page/detail/process-detail.component.ts b/src/app/process-page/detail/process-detail.component.ts index af6e3238a6..f6b628f0f8 100644 --- a/src/app/process-page/detail/process-detail.component.ts +++ b/src/app/process-page/detail/process-detail.component.ts @@ -1,12 +1,13 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; import { ProcessOutputDataService } from '../../core/data/process-output-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ProcessOutput } from '../processes/process-output.model'; import { Process } from '../processes/process.model'; -import { map, switchMap } from 'rxjs/operators'; +import { finalize, map, switchMap, take } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { AlertType } from '../../shared/alert/aletr-type'; import { ProcessDataService } from '../../core/data/processes/process-data.service'; @@ -44,11 +45,21 @@ export class ProcessDetailComponent implements OnInit { */ outputLogs$: Observable; + /** + * Boolean on whether or not to show the output logs + */ + showOutputLogs = false; + /** + * When it's retrieving the output logs from backend, to show loading component + */ + retrievingOutputLogs$ = new BehaviorSubject(false); + constructor(protected route: ActivatedRoute, protected router: Router, protected processService: ProcessDataService, protected processOutputService: ProcessOutputDataService, - protected nameService: DSONameService) { + protected nameService: DSONameService, + private zone: NgZone) { } /** @@ -65,17 +76,6 @@ export class ProcessDetailComponent implements OnInit { getFirstSucceededRemoteDataPayload(), switchMap((process: Process) => this.processService.getFiles(process.processId)) ); - - const processOutputRD$: Observable> = this.processRD$.pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) - ); - this.outputLogs$ = processOutputRD$.pipe( - getFirstSucceededRemoteDataPayload(), - map((processOutput: ProcessOutput) => { - return processOutput.logs; - }) - ) } /** @@ -86,4 +86,27 @@ export class ProcessDetailComponent implements OnInit { return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown'; } + /** + * Retrieves the process logs, while setting the loading subject to true. + * Sets the outputLogs when retrieved and sets the showOutputLogs boolean to show them and hide the button. + */ + showProcessOutputLogs() { + this.retrievingOutputLogs$.next(true); + this.zone.runOutsideAngular(() => { + const processOutputRD$: Observable> = this.processRD$.pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((process: Process) => this.processOutputService.findByHref(process._links.output.href)) + ); + this.outputLogs$ = processOutputRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((processOutput: ProcessOutput) => { + this.showOutputLogs = true; + return processOutput.logs; + }), + finalize(() => this.zone.run(() => this.retrievingOutputLogs$.next(false))), + ) + }); + this.outputLogs$.pipe(take(1)).subscribe(); + } + } diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index 57c749e1cb..84821a2574 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -24,7 +24,7 @@ export class ProcessPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.processService.findById(route.params.id, followLink('script'), followLink('output') ).pipe( + return this.processService.findById(route.params.id, followLink('script')).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 75db3eceb1..036fd87ec8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2085,6 +2085,10 @@ "process.detail.output" : "Process Output", + "process.detail.logs.button": "Retrieve process output", + + "process.detail.logs.loading": "Retrieving", + "process.detail.logs.none": "This process has no output yet", "process.detail.output-files" : "Output Files", From d9c177b10018f28493f18976daa761fa307098e4 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 24 Sep 2020 13:02:38 +0200 Subject: [PATCH 05/78] added RequestService --- .../create-collection-page.component.ts | 6 ++++-- .../delete-collection-page.component.ts | 6 ++++-- .../create-community-page.component.ts | 6 ++++-- .../delete-community-page.component.ts | 7 +++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index ae31b94c3d..a38739c407 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -7,6 +7,7 @@ import { Collection } from '../../core/shared/collection.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import {RequestService} from '../../core/data/request.service'; /** * Component that represents the page where a user can create a new Collection @@ -26,8 +27,9 @@ export class CreateCollectionPageComponent extends CreateComColPageComponent Date: Thu, 24 Sep 2020 13:03:29 +0200 Subject: [PATCH 06/78] cache cleared with parent community reference once a collection or a community is created or deleted --- .../create-comcol-page.component.ts | 70 +++++++++++++------ .../delete-comcol-page.component.ts | 47 ++++++++++--- 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 4a7cd9afb1..f55a7f0156 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -2,18 +2,24 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import {flatMap, map, take} from 'rxjs/operators'; import { ComColDataService } from '../../../core/data/comcol-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { RouteService } from '../../../core/services/route.service'; import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../empty.util'; +import {hasValue, isEmpty, isNotEmpty, isNotUndefined} from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; import { RequestParam } from '../../../core/cache/models/request-param.model'; +import {RequestService} from '../../../core/data/request.service'; +import {Collection} from '../../../core/shared/collection.model'; /** * Component representing the create page for communities and collections @@ -54,7 +60,8 @@ export class CreateComColPageComponent implements protected routeService: RouteService, protected router: Router, protected notificationsService: NotificationsService, - protected translate: TranslateService + protected translate: TranslateService, + protected requestService: RequestService ) { } @@ -76,25 +83,29 @@ export class CreateComColPageComponent implements const dso = event.dso; const uploader = event.uploader; - this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { + this.parentUUID$.pipe( + take(1), + flatMap((uuid: string) => { const params = uuid ? [new RequestParam('parent', uuid)] : []; - this.dsoDataService.create(dso, ...params) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - this.newUUID = dsoRD.payload.uuid; - if (uploader.queue.length > 0) { - this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); - } else { - this.navigateToNewPage(); - } - this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); + return this.dsoDataService.create(dso, ...params) + .pipe(getFirstSucceededRemoteDataPayload() + ) + })) + .subscribe((dsoRD: TDomain) => { + if (isNotUndefined(dsoRD)) { + this.newUUID = dsoRD.uuid; + if (uploader.queue.length > 0) { + this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); + }); + } else { + this.navigateToNewPage(); } - }); - }); + this.refreshCache(dsoRD); + } + this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); + }); } /** @@ -106,4 +117,21 @@ export class CreateComColPageComponent implements } } + private refreshCache(dso: TDomain) { + const parentCommunityUrl = this.parentCommunityUrl(dso as any); + if (!hasValue(parentCommunityUrl)) { + return; + } + this.dsoDataService.findByHref(parentCommunityUrl).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((pc: TDomain) => isEmpty(pc) ? 'communities/search/top' : pc.id), + take(1) + ).subscribe((href: string) => this.requestService.removeByHrefSubstring(href)); + } + + private parentCommunityUrl(dso: Collection | Community) { + const parentCommunity = dso._links.parentCommunity; + return isNotEmpty(parentCommunity) ? parentCommunity.href : null; + } } diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts index d07d7be032..29ec4f7b19 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts @@ -1,13 +1,18 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../../core/data/remote-data'; -import { first, map } from 'rxjs/operators'; -import { DataService } from '../../../core/data/data.service'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { NotificationsService } from '../../notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { RestResponse } from '../../../core/cache/response.models'; +import {Component, OnInit} from '@angular/core'; +import {Observable} from 'rxjs'; +import {ActivatedRoute, Router} from '@angular/router'; +import {RemoteData} from '../../../core/data/remote-data'; +import {first, map, take} from 'rxjs/operators'; +import {DataService} from '../../../core/data/data.service'; +import {DSpaceObject} from '../../../core/shared/dspace-object.model'; +import {NotificationsService} from '../../notifications/notifications.service'; +import {TranslateService} from '@ngx-translate/core'; +import {RestResponse} from '../../../core/cache/response.models'; +import {hasValue, isEmpty, isNotEmpty} from '../../empty.util'; +import {RequestService} from '../../../core/data/request.service'; +import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators'; +import {Community} from '../../../core/shared/community.model'; +import {Collection} from '../../../core/shared/collection.model'; /** * Component representing the delete page for communities and collections @@ -31,7 +36,8 @@ export class DeleteComColPageComponent implements protected router: Router, protected route: ActivatedRoute, protected notifications: NotificationsService, - protected translate: TranslateService + protected translate: TranslateService, + protected requestService: RequestService ) { } @@ -50,6 +56,7 @@ export class DeleteComColPageComponent implements if (response.isSuccessful) { const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success'); this.notifications.success(successMessage) + this.refreshCache(dso); } else { const errorMessage = this.translate.instant((dso as any).type + '.delete.notification.fail'); this.notifications.error(errorMessage) @@ -65,4 +72,22 @@ export class DeleteComColPageComponent implements onCancel(dso: TDomain) { this.router.navigate([this.frontendURL + '/' + dso.uuid + '/edit']); } + + private refreshCache(dso: TDomain) { + const parentCommunity = this.parentCommunityUrl(dso as any); + if (!hasValue(parentCommunity)) { + return; + } + this.dsoDataService.findByHref(parentCommunity).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((pc: TDomain) => isEmpty(pc) ? 'communities/search/top' : pc.id ), + take(1) + ).subscribe((id: string) => this.requestService.removeByHrefSubstring(id)); + } + + private parentCommunityUrl(dso: Collection | Community): string { + const parentCommunity = dso._links.parentCommunity; + return isNotEmpty(parentCommunity) ? parentCommunity.href : null; + } } From 6a016cd17f49c95e0e58709fd0c9dc3b5cceff39 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 20 Oct 2020 16:40:01 +0200 Subject: [PATCH 07/78] 74199: ItemResolver item to dso refactor --- .../abstract-item-update/abstract-item-update.component.ts | 2 +- src/app/+item-page/edit-item-page/edit-item-page.component.ts | 2 +- .../item-authorizations/item-authorizations.component.spec.ts | 2 +- .../item-authorizations/item-authorizations.component.ts | 2 +- .../item-bitstreams/item-bitstreams.component.spec.ts | 2 +- .../item-collection-mapper.component.spec.ts | 2 +- .../item-collection-mapper/item-collection-mapper.component.ts | 2 +- .../edit-item-page/item-delete/item-delete.component.spec.ts | 2 +- .../item-metadata/item-metadata.component.spec.ts | 2 +- .../edit-item-page/item-move/item-move.component.spec.ts | 2 +- .../+item-page/edit-item-page/item-move/item-move.component.ts | 2 +- .../edit-item-page/item-private/item-private.component.spec.ts | 2 +- .../edit-item-page/item-public/item-public.component.spec.ts | 2 +- .../item-reinstate/item-reinstate.component.spec.ts | 2 +- .../item-relationships/item-relationships.component.spec.ts | 2 +- .../item-withdraw/item-withdraw.component.spec.ts | 2 +- .../abstract-simple-item-action.component.spec.ts | 2 +- .../simple-item-action/abstract-simple-item-action.component.ts | 2 +- src/app/+item-page/full/full-item-page.component.spec.ts | 2 +- src/app/+item-page/item-page-routing.module.ts | 2 +- src/app/+item-page/simple/item-page.component.spec.ts | 2 +- src/app/+item-page/simple/item-page.component.ts | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index bde2b5a1b0..f3055d3e51 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -48,7 +48,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl ngOnInit(): void { observableCombineLatest(this.route.data, this.route.parent.data).pipe( map(([data, parentData]) => Object.assign({}, data, parentData)), - map((data) => data.item), + map((data) => data.dso), first(), map((data: RemoteData) => data.payload) ).subscribe((item: Item) => { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts index 655582064c..2bd9a30ca3 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts @@ -47,7 +47,7 @@ export class EditItemPageComponent implements OnInit { this.pages = this.route.routeConfig.children .map((child: any) => child.path) .filter((path: string) => isNotEmpty(path)); // ignore reroutes - this.itemRD$ = this.route.data.pipe(map((data) => data.item)); + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)); } /** diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index c687c829eb..dcf70a30cb 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -74,7 +74,7 @@ describe('ItemAuthorizationsComponent test suite', () => { const routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject(item) + dso: createSuccessfulRemoteDataObject(item) }) }; diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 8153990a02..8b89de7c89 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -75,7 +75,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { */ ngOnInit(): void { this.item$ = this.route.data.pipe( - map((data) => data.item), + map((data) => data.dso), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((item: Item) => this.linkService.resolveLink( item, diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index f86c57d69e..5f6e3a06c4 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -140,7 +140,7 @@ describe('ItemBitstreamsComponent', () => { }); route = Object.assign({ parent: { - data: observableOf({ item: createMockRD(item) }) + data: observableOf({ dso: createMockRD(item) }) }, data: observableOf({}), url: url diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index 15b860a782..9aeb1522a6 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -89,7 +89,7 @@ describe('ItemCollectionMapperComponent', () => { clearDiscoveryRequests: () => {} /* tslint:enable:no-empty */ }); - const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD }); + const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockItemRD }); const translateServiceStub = { get: () => of('test-message of item ' + mockItem.name), onLangChange: new EventEmitter(), diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 1409e06ddb..df406f826b 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -92,7 +92,7 @@ export class ItemCollectionMapperComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadCollectionLists(); } diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index 18cbd6e855..e7b454e92b 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -138,7 +138,7 @@ describe('ItemDeleteComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject(mockItem) + dso: createSuccessfulRemoteDataObject(mockItem) }) }; diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index ed9ab4a891..f30b5cc3b0 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -130,7 +130,7 @@ describe('ItemMetadataComponent', () => { routeStub = { data: observableOf({}), parent: { - data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } }; paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]); diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index 77aefe2356..c8c49b118b 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -44,7 +44,7 @@ describe('ItemMoveComponent', () => { const routeStub = { data: observableOf({ - item: new RemoteData(false, false, true, null, { + dso: new RemoteData(false, false, true, null, { id: 'item1' }) }) diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index abadd2ec4a..1a544af7dc 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -55,7 +55,7 @@ export class ItemMoveComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.data.pipe(map((data) => data.item), getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getSucceededRemoteData()) as Observable>; this.itemRD$.subscribe((rd) => { this.itemId = rd.payload.id; } diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts index 95b08a9936..52ccbc2133 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemPrivateComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts index 53df20bf04..1143874709 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemPublicComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index 5e75b59292..005f330df9 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemReinstateComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 5e657ae09d..17fede115e 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -142,7 +142,7 @@ describe('ItemRelationshipsComponent', () => { routeStub = { data: observableOf({}), parent: { - data: observableOf({ item: new RemoteData(false, false, true, null, item) }) + data: observableOf({ dso: new RemoteData(false, false, true, null, item) }) } }; diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts index 249298c092..aa6a3e537a 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemWithdrawComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts index e6c5cfefc0..31c8a6f808 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts @@ -74,7 +74,7 @@ describe('AbstractSimpleItemActionComponent', () => { routeStub = { data: observableOf({ - item: createSuccessfulRemoteDataObject({ + dso: createSuccessfulRemoteDataObject({ id: 'fake-id' }) }) diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts index ca347e1298..1bd8782a30 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts @@ -42,7 +42,7 @@ export class AbstractSimpleItemActionComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.data.pipe( - map((data) => data.item), + map((data) => data.dso), getSucceededRemoteData() )as Observable>; diff --git a/src/app/+item-page/full/full-item-page.component.spec.ts b/src/app/+item-page/full/full-item-page.component.spec.ts index 0512ea2fef..af966a2530 100644 --- a/src/app/+item-page/full/full-item-page.component.spec.ts +++ b/src/app/+item-page/full/full-item-page.component.spec.ts @@ -34,7 +34,7 @@ const mockItem: Item = Object.assign(new Item(), { } }); const routeStub = Object.assign(new ActivatedRouteStub(), { - data: observableOf({ item: createSuccessfulRemoteDataObject(mockItem) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }) }); const metadataServiceStub = { /* tslint:disable:no-empty */ diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index e4f17326a4..022d905ff3 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -20,7 +20,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; { path: ':id', resolve: { - item: ItemPageResolver, + dso: ItemPageResolver, breadcrumb: ItemBreadcrumbResolver }, runGuardsAndResolvers: 'always', diff --git a/src/app/+item-page/simple/item-page.component.spec.ts b/src/app/+item-page/simple/item-page.component.spec.ts index 6c26e75908..c2dd0b7015 100644 --- a/src/app/+item-page/simple/item-page.component.spec.ts +++ b/src/app/+item-page/simple/item-page.component.spec.ts @@ -37,7 +37,7 @@ describe('ItemPageComponent', () => { /* tslint:enable:no-empty */ }; const mockRoute = Object.assign(new ActivatedRouteStub(), { - data: observableOf({ item: createSuccessfulRemoteDataObject(mockItem) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }) }); beforeEach(async(() => { diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 10deef23e4..591a093cc9 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -55,7 +55,7 @@ export class ItemPageComponent implements OnInit { */ ngOnInit(): void { this.itemRD$ = this.route.data.pipe( - map((data) => data.item as RemoteData), + map((data) => data.dso as RemoteData), redirectToPageNotFoundOn404(this.router) ); this.metadataService.processRemoteData(this.itemRD$); From ac44bb9cb9c5fd8d77298ff35dbb9d56ef66c6e0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 20 Oct 2020 17:54:12 +0200 Subject: [PATCH 08/78] 74199: Admin search dialogs - intermediate commit --- src/app/core/shared/context.model.ts | 1 + .../dso-selector/dso-selector.component.html | 2 +- .../dso-selector/dso-selector.component.ts | 6 ++ ...sidebar-search-list-element.component.html | 1 + ...n-sidebar-search-list-element.component.ts | 17 ++++++ ...sidebar-search-list-element.component.html | 3 + .../sidebar-search-list-element.component.ts | 56 +++++++++++++++++++ src/app/shared/shared.module.ts | 6 +- 8 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html create mode 100644 src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html create mode 100644 src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index 4699a7977b..207c1fb99d 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -13,4 +13,5 @@ export enum Context { EntitySearchModal = 'EntitySearchModal', AdminSearch = 'adminSearch', AdminWorkflowSearch = 'adminWorkflowSearch', + SideBarSearchModal = 'sideBarSearchModal', } diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 92ddf4cf36..8a2f9272c4 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -16,6 +16,6 @@ title="{{ listEntry.indexableObject.name }}" (click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement> + [linkType]=linkTypes.None [context]="context"> diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index 37c9a99f59..d0404d61c9 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -21,6 +21,7 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { SearchResult } from '../../search/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; +import { Context } from '../../../core/shared/context.model'; @Component({ selector: 'ds-dso-selector', @@ -85,6 +86,11 @@ export class DSOSelectorComponent implements OnInit { */ linkTypes = CollectionElementLinkType; + /** + * This component's context to display listable objects for + */ + context = Context.SideBarSearchModal; + constructor(private searchService: SearchService) { } diff --git a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html new file mode 100644 index 0000000000..be25f1af49 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html @@ -0,0 +1 @@ +Test display for sidebar-search list elements diff --git a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..49724a9309 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts @@ -0,0 +1,17 @@ +import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Component } from '@angular/core'; +import { Context } from '../../../../../core/shared/context.model'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { SidebarSearchListElementComponent } from '../../sidebar-search-list-element.component'; + +@listableObjectComponent('PublicationSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-publication-sidebar-search-list-element', + templateUrl: '../../sidebar-search-list-element.component.html' +}) +export class PublicationSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + +} diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html new file mode 100644 index 0000000000..b0fe1e58d2 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html @@ -0,0 +1,3 @@ +
{{ parentTitle$ | async }}
+
{{ title }}
+
{{ description }}
diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..0489f9f3b7 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts @@ -0,0 +1,56 @@ +import { SearchResult } from '../../search/search-result.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { SearchResultListElementComponent } from '../search-result-list-element/search-result-list-element.component'; +import { Component } from '@angular/core'; +import { hasValue } from '../../empty.util'; +import { Observable } from 'rxjs/internal/Observable'; +import { TruncatableService } from '../../truncatable/truncatable.service'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { find, map } from 'rxjs/operators'; +import { ChildHALResource } from '../../../core/shared/child-hal-resource.model'; +import { followLink } from '../../utils/follow-link-config.model'; +import { RemoteData } from '../../../core/data/remote-data'; + +@Component({ + selector: 'ds-sidebar-search-list-element', + templateUrl: './sidebar-search-list-element.component.html' +}) +export class SidebarSearchListElementComponent, K extends DSpaceObject> extends SearchResultListElementComponent { + parentTitle$: Observable; + title: string; + description: string; + + public constructor(protected truncatableService: TruncatableService, + protected linkService: LinkService) { + super(truncatableService); + } + + ngOnInit(): void { + super.ngOnInit(); + if (hasValue(this.dso)) { + this.parentTitle$ = this.getParentTitle(); + this.title = this.getTitle(); + this.description = this.getDescription(); + } + } + + getTitle(): string { + return this.firstMetadataValue('dc.title'); + } + + getDescription(): string { + // TODO: Expand description + return this.firstMetadataValue('dc.publisher'); + } + + getParentTitle(): Observable { + // TODO: Remove cast to "any" and replace with proper type-check + const propertyName = (this.dso as any).getParentLinkKey(); + return this.linkService.resolveLink(this.dso, followLink(propertyName))[propertyName].pipe( + find((parentRD: RemoteData) => parentRD.hasSucceeded || parentRD.statusCode === 204), + map((parentRD: RemoteData) => { + return parentRD.payload.firstMetadataValue('dc.title'); + }) + ); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 5f35ed4ceb..5b849eb403 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -210,6 +210,8 @@ import { CollectionDropdownComponent } from './collection-dropdown/collection-dr import { DsSelectComponent } from './ds-select/ds-select.component'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { CurationFormComponent } from '../curation-form/curation-form.component'; +import { PublicationSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; +import { SidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -483,7 +485,9 @@ const ENTRY_COMPONENTS = [ CurationFormComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, - VocabularyTreeviewComponent + VocabularyTreeviewComponent, + SidebarSearchListElementComponent, + PublicationSidebarSearchListElementComponent, ]; const SHARED_ITEM_PAGE_COMPONENTS = [ From f79ea2b844c5eb7456fb0e9882ec0ae51602b611 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Oct 2020 13:24:21 +0200 Subject: [PATCH 09/78] 74199: Admin search dialogs - sidebar search list element implementations - intermediate commit --- ...e-sidebar-search-list-element.component.ts | 38 ++++++ ...e-sidebar-search-list-element.component.ts | 38 ++++++ ...l-sidebar-search-list-element.component.ts | 31 +++++ .../journal-entities.module.ts | 8 +- ...t-sidebar-search-list-element.component.ts | 33 +++++ ...n-sidebar-search-list-element.component.ts | 59 +++++++++ ...t-sidebar-search-list-element.component.ts | 26 ++++ .../research-entities.module.ts | 8 +- ...sidebar-search-list-element.component.html | 1 - ...n-sidebar-search-list-element.component.ts | 4 + ...sidebar-search-list-element.component.html | 6 +- .../sidebar-search-list-element.component.ts | 113 ++++++++++++++++-- src/assets/i18n/en.json5 | 2 + 13 files changed, 349 insertions(+), 18 deletions(-) create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.ts create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.ts create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.ts delete mode 100644 src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..026a9be15c --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.ts @@ -0,0 +1,38 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@listableObjectComponent('JournalIssueSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-journal-issue-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "JournalIssue" within the context of + * a sidebar search modal + */ +export class JournalIssueSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the description of the Journal Issue by returning its volume number(s) and/or issue number(s) + */ + getDescription(): string { + const volumeNumbers = this.allMetadataValues(['publicationvolume.volumeNumber']); + const issueNumbers = this.allMetadataValues(['publicationissue.issueNumber']); + let description = ''; + if (isNotEmpty(volumeNumbers)) { + description += volumeNumbers.join(', '); + } + if (isNotEmpty(description) && isNotEmpty(issueNumbers)) { + description += ' - '; + } + if (isNotEmpty(issueNumbers)) { + description += issueNumbers.join(', '); + } + return this.undefinedIfEmpty(description); + } +} diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..ce99d14406 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.ts @@ -0,0 +1,38 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@listableObjectComponent('JournalVolumeSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-journal-volume-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "JournalVolume" within the context of + * a sidebar search modal + */ +export class JournalVolumeSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the description of the Journal Volume by returning the journal title and volume number(s) (between parentheses) + */ + getDescription(): string { + const titles = this.allMetadataValues(['journal.title']); + const numbers = this.allMetadataValues(['publicationvolume.volumeNumber']); + let description = ''; + if (isNotEmpty(titles)) { + description += titles.join(', '); + } + if (isNotEmpty(numbers)) { + if (isNotEmpty(description)) { + description += ' '; + } + description += numbers.map((n) => `(${n})`).join(' '); + } + return this.undefinedIfEmpty(description); + } +} diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..f222298ee3 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.ts @@ -0,0 +1,31 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@listableObjectComponent('JournalSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-journal-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "Journal" within the context of + * a sidebar search modal + */ +export class JournalSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the description of the Journal by returning its ISSN(s) + */ + getDescription(): string { + const issns = this.allMetadataValues(['creativeworkseries.issn']); + let description = ''; + if (isNotEmpty(issns)) { + description += issns.join(', '); + } + return this.undefinedIfEmpty(description); + } +} diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index d00eae1e54..11ce6c4c2a 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -18,6 +18,9 @@ import { JournalIssueSearchResultListElementComponent } from './item-list-elemen import { JournalVolumeSearchResultListElementComponent } from './item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component'; import { JournalIssueSearchResultGridElementComponent } from './item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component'; import { JournalVolumeSearchResultGridElementComponent } from './item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component'; +import { JournalVolumeSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component'; +import { JournalIssueSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component'; +import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component'; const ENTRY_COMPONENTS = [ JournalComponent, @@ -34,7 +37,10 @@ const ENTRY_COMPONENTS = [ JournalVolumeSearchResultListElementComponent, JournalIssueSearchResultGridElementComponent, JournalVolumeSearchResultGridElementComponent, - JournalSearchResultGridElementComponent + JournalSearchResultGridElementComponent, + JournalVolumeSidebarSearchListElementComponent, + JournalIssueSidebarSearchListElementComponent, + JournalSidebarSearchListElementComponent, ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..54fed3125f --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.ts @@ -0,0 +1,33 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-org-unit-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "OrgUnit" within the context of + * a sidebar search modal + */ +export class OrgUnitSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the title of the Org Unit by returning its legal name + */ + getTitle(): string { + return this.firstMetadataValue('organization.legalName'); + } + + /** + * Get the description of the Org Unit by returning its dc.description + */ + getDescription(): string { + return this.firstMetadataValue('dc.description'); + } +} diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..9cbf66d040 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts @@ -0,0 +1,59 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { TranslateService } from '@ngx-translate/core'; + +@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-person-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "Person" within the context of + * a sidebar search modal + */ +export class PersonSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + constructor(protected truncatableService: TruncatableService, + protected linkService: LinkService, + protected translateService: TranslateService) { + super(truncatableService, linkService); + } + + /** + * Get the title of the Person by returning a combination of its family name and given name (or "No name found") + */ + getTitle(): string { + const familyName = this.firstMetadataValue('person.familyName'); + const givenName = this.firstMetadataValue('person.givenName'); + let title = ''; + if (isNotEmpty(familyName)) { + title = familyName; + } + if (isNotEmpty(title)) { + title += ', '; + } + if (isNotEmpty(givenName)) { + title += givenName; + } + return this.defaultIfEmpty(title, this.translateService.instant('person.listelement.no-title')); + } + + /** + * Get the description of the Person by returning its job title(s) + */ + getDescription(): string { + const titles = this.allMetadataValues(['person.jobTitle']); + let description = ''; + if (isNotEmpty(titles)) { + description += titles.join(', '); + } + return this.undefinedIfEmpty(description); + } +} diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..00124cf497 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.ts @@ -0,0 +1,26 @@ +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 { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { Component } from '@angular/core'; +import { SidebarSearchListElementComponent } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@listableObjectComponent('ProjectSearchResult', ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-project-sidebar-search-list-element', + templateUrl: '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "Project" within the context of + * a sidebar search modal + */ +export class ProjectSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Projects currently don't support a description + */ + getDescription(): string { + return undefined; + } +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index cef3b4539b..1f50ab830a 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -26,6 +26,9 @@ import { NameVariantModalComponent } from './submission/name-variant-modal/name- import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component'; import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component'; import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component'; +import { OrgUnitSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component'; +import { PersonSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component'; +import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -50,7 +53,10 @@ const ENTRY_COMPONENTS = [ NameVariantModalComponent, OrgUnitSearchResultListSubmissionElementComponent, OrgUnitInputSuggestionsComponent, - ExternalSourceEntryListSubmissionElementComponent + ExternalSourceEntryListSubmissionElementComponent, + OrgUnitSidebarSearchListElementComponent, + PersonSidebarSearchListElementComponent, + ProjectSidebarSearchListElementComponent, ]; @NgModule({ diff --git a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html deleted file mode 100644 index be25f1af49..0000000000 --- a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.html +++ /dev/null @@ -1 +0,0 @@ -Test display for sidebar-search list elements diff --git a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts index 49724a9309..fb79a4924e 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts +++ b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.ts @@ -12,6 +12,10 @@ import { SidebarSearchListElementComponent } from '../../sidebar-search-list-ele selector: 'ds-publication-sidebar-search-list-element', templateUrl: '../../sidebar-search-list-element.component.html' }) +/** + * Component displaying a list element for a {@link ItemSearchResult} of type "Publication" within the context of + * a sidebar search modal + */ export class PublicationSidebarSearchListElementComponent extends SidebarSearchListElementComponent { } diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html index b0fe1e58d2..adcf23dcf7 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html @@ -1,3 +1,3 @@ -
{{ parentTitle$ | async }}
-
{{ title }}
-
{{ description }}
+
{{ parentTitle$ | async }}
+
{{ title }}
+
{{ description }}
diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts index 0489f9f3b7..71547854f9 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts @@ -2,7 +2,7 @@ import { SearchResult } from '../../search/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { SearchResultListElementComponent } from '../search-result-list-element/search-result-list-element.component'; import { Component } from '@angular/core'; -import { hasValue } from '../../empty.util'; +import { hasValue, isNotEmpty } from '../../empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { TruncatableService } from '../../truncatable/truncatable.service'; import { LinkService } from '../../../core/cache/builders/link.service'; @@ -10,14 +10,31 @@ import { find, map } from 'rxjs/operators'; import { ChildHALResource } from '../../../core/shared/child-hal-resource.model'; import { followLink } from '../../utils/follow-link-config.model'; import { RemoteData } from '../../../core/data/remote-data'; +import { of as observableOf } from 'rxjs'; @Component({ selector: 'ds-sidebar-search-list-element', templateUrl: './sidebar-search-list-element.component.html' }) +/** + * Component displaying a list element for a {@link SearchResult} in the sidebar search modal + * It displays the name of the parent, title and description of the object. All of which are customizable in the child + * component by overriding the relevant methods of this component + */ export class SidebarSearchListElementComponent, K extends DSpaceObject> extends SearchResultListElementComponent { + /** + * Observable for the title of the parent object (displayed above the object's title) + */ parentTitle$: Observable; + + /** + * The title for the object to display + */ title: string; + + /** + * A description to display below the title + */ description: string; public constructor(protected truncatableService: TruncatableService, @@ -25,6 +42,9 @@ export class SidebarSearchListElementComponent, K exte super(truncatableService); } + /** + * Initialise the component variables + */ ngOnInit(): void { super.ngOnInit(); if (hasValue(this.dso)) { @@ -34,23 +54,92 @@ export class SidebarSearchListElementComponent, K exte } } + /** + * Get the title of the object's parent + * Retrieve the parent by using the object's parent link and retrieving its 'dc.title' metadata + */ + getParentTitle(): Observable { + return this.getParent().pipe( + map((parentRD: RemoteData) => { + return parentRD ? parentRD.payload.firstMetadataValue('dc.title') : undefined; + }) + ); + } + + /** + * Get the parent of the object + */ + getParent(): Observable> { + if (typeof (this.dso as any).getParentLinkKey === 'function') { + const propertyName = (this.dso as any).getParentLinkKey(); + return this.linkService.resolveLink(this.dso, followLink(propertyName))[propertyName].pipe( + find((parentRD: RemoteData) => parentRD.hasSucceeded || parentRD.statusCode === 204) + ); + } + return observableOf(undefined); + } + + /** + * Get the title of the object + * Default: "dc.title" + */ getTitle(): string { return this.firstMetadataValue('dc.title'); } + /** + * Get the description of the object + * Default: "(dc.publisher, dc.date.issued) authors" + */ getDescription(): string { - // TODO: Expand description - return this.firstMetadataValue('dc.publisher'); + const publisher = this.firstMetadataValue('dc.publisher'); + const date = this.firstMetadataValue('dc.date.issued'); + const authors = this.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); + let description = ''; + if (isNotEmpty(publisher) || isNotEmpty(date)) { + description += '('; + } + if (isNotEmpty(publisher)) { + description += publisher; + } + if (isNotEmpty(date)) { + if (isNotEmpty(publisher)) { + description += ', '; + } + description += date; + } + if (isNotEmpty(description)) { + description += ') '; + } + if (isNotEmpty(authors)) { + authors.forEach((author, i) => { + description += author; + if (i < (authors.length - 1)) { + description += '; '; + } + }); + } + return this.undefinedIfEmpty(description); } - getParentTitle(): Observable { - // TODO: Remove cast to "any" and replace with proper type-check - const propertyName = (this.dso as any).getParentLinkKey(); - return this.linkService.resolveLink(this.dso, followLink(propertyName))[propertyName].pipe( - find((parentRD: RemoteData) => parentRD.hasSucceeded || parentRD.statusCode === 204), - map((parentRD: RemoteData) => { - return parentRD.payload.firstMetadataValue('dc.title'); - }) - ); + /** + * Return undefined if the provided string is empty + * @param value Value to check + */ + undefinedIfEmpty(value: string) { + return this.defaultIfEmpty(value, undefined); + } + + /** + * Return a default value if the provided string is empty + * @param value Value to check + * @param def Default in case value is empty + */ + defaultIfEmpty(value: string, def: string) { + if (isNotEmpty(value)) { + return value; + } else { + return def; + } } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 84d874388c..a6aaad98ea 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2194,6 +2194,8 @@ "person.listelement.badge": "Person", + "person.listelement.no-title": "No name found", + "person.page.birthdate": "Birth Date", "person.page.email": "Email Address", From 8b2b28df2508c923e06209cef75e06133e981d8e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 21 Oct 2020 18:04:03 +0200 Subject: [PATCH 10/78] 74199: Infinite scrollable dso-selector + collection/community implementation + authorized-collection-selector --- ...uthorized-collection-selector.component.ts | 40 ++++++ .../dso-selector/dso-selector.component.html | 12 +- .../dso-selector/dso-selector.component.scss | 5 + .../dso-selector/dso-selector.component.ts | 123 ++++++++++++++---- ...create-item-parent-selector.component.html | 3 +- ...n-sidebar-search-list-element.component.ts | 24 ++++ ...y-sidebar-search-list-element.component.ts | 24 ++++ src/app/shared/shared.module.ts | 9 +- src/styles/_custom_variables.scss | 2 + 9 files changed, 212 insertions(+), 30 deletions(-) create mode 100644 src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts create mode 100644 src/app/shared/dso-selector/dso-selector/dso-selector.component.scss create mode 100644 src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts new file mode 100644 index 0000000000..ff9fa81368 --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { DSOSelectorComponent } from '../dso-selector.component'; +import { SearchService } from '../../../../core/shared/search/search.service'; +import { CollectionDataService } from '../../../../core/data/collection-data.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { map } from 'rxjs/operators'; +import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; +import { SearchResult } from '../../../search/search-result.model'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-authorized-collection-selector', + templateUrl: '../dso-selector.component.html' +}) +/** + * Component rendering a list of collections to select from + */ +export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { + constructor(protected searchService: SearchService, + protected collectionDataService: CollectionDataService) { + super(searchService); + } + + /** + * Perform a search for authorized collections with the current query and page + * @param query Query to search objects for + * @param page Page to retrieve + */ + search(query: string, page: number): Observable>> { + return this.collectionDataService.getAuthorizedCollection(query, Object.assign({ + currentPage: page, + elementsPerPage: this.defaultPagination.pageSize + })).pipe( + getFirstSucceededRemoteDataPayload(), + map((list) => new PaginatedList(list.pageInfo, list.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col })))) + ); + } +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 8a2f9272c4..a0b6aff2a3 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -7,15 +7,23 @@
+
- +
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss new file mode 100644 index 0000000000..37d2ebeca7 --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.scss @@ -0,0 +1,5 @@ +.scrollable-menu { + height: auto; + max-height: $dso-selector-list-max-height; + overflow-x: hidden; +} diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index d0404d61c9..8d2d6f758b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -3,29 +3,33 @@ import { ElementRef, EventEmitter, Input, + OnDestroy, OnInit, Output, QueryList, ViewChildren } from '@angular/core'; import { FormControl } from '@angular/forms'; - -import { Observable } from 'rxjs'; -import { debounceTime, startWith, switchMap } from 'rxjs/operators'; +import { debounceTime, startWith, switchMap, tap } from 'rxjs/operators'; import { SearchService } from '../../../core/shared/search/search.service'; import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { SearchResult } from '../../search/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { Context } from '../../../core/shared/context.model'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { hasValue } from '../../empty.util'; +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { SearchResult } from '../../search/search-result.model'; @Component({ selector: 'ds-dso-selector', - // styleUrls: ['./dso-selector.component.scss'], + styleUrls: ['./dso-selector.component.scss'], templateUrl: './dso-selector.component.html' }) @@ -33,7 +37,7 @@ import { Context } from '../../../core/shared/context.model'; * Component to render a list of DSO's of which one can be selected * The user can search the list by using the input field */ -export class DSOSelectorComponent implements OnInit { +export class DSOSelectorComponent implements OnInit, OnDestroy { /** * The view mode of the listed objects */ @@ -64,12 +68,29 @@ export class DSOSelectorComponent implements OnInit { /** * Default pagination for this feature */ - private defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any; + defaultPagination = { id: 'dso-selector', currentPage: 1, pageSize: 5 } as any; /** * List with search results of DSpace objects for the current query */ - listEntries$: Observable>>>; + listEntries: Array> = []; + + /** + * The current page to load + * Dynamically goes up as the user scrolls down until it reaches the last page possible + */ + currentPage$ = new BehaviorSubject(1); + + /** + * Whether or not the list contains a next page to load + * This allows us to avoid next pages from trying to load when there are none + */ + hasNextPage = false; + + /** + * Whether or not the list should be reset next time it receives a page to load + */ + resetList = false; /** * List of element references to all elements @@ -91,31 +112,76 @@ export class DSOSelectorComponent implements OnInit { */ context = Context.SideBarSearchModal; - constructor(private searchService: SearchService) { + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + constructor(protected searchService: SearchService) { } /** - * Fills the listEntries$ variable with search results based on the input field's current value + * Fills the listEntries variable with search results based on the input field's current value and the current page * The search will always start with the initial currentDSOId value */ ngOnInit(): void { this.input.setValue(this.currentDSOId); this.typesString = this.types.map((type: string) => type.toString().toLowerCase()).join(', '); - this.listEntries$ = this.input.valueChanges - .pipe( + + this.subs.push(observableCombineLatest( + this.input.valueChanges.pipe( debounceTime(this.debounceTime), startWith(this.currentDSOId), - switchMap((query) => { - return this.searchService.search( - new PaginatedSearchOptions({ - query: query, - dsoTypes: this.types, - pagination: this.defaultPagination - }) - ) - } - ) - ) + tap(() => this.currentPage$.next(1)) + ), + this.currentPage$ + ).pipe( + switchMap(([query, page]: [string, number]) => { + if (page === 1) { + // The first page is loading, this means we should reset the list instead of adding to it + this.resetList = true; + } + return this.search(query, page); + }) + ).subscribe((list) => { + if (this.resetList) { + this.listEntries = list.page; + this.resetList = false; + } else { + this.listEntries.push(...list.page); + } + // Check if there are more pages available after the current one + this.hasNextPage = list.totalElements > this.listEntries.length; + })); + } + + /** + * Perform a search for the current query and page + * @param query Query to search objects for + * @param page Page to retrieve + */ + search(query: string, page: number): Observable>> { + return this.searchService.search( + new PaginatedSearchOptions({ + query: query, + dsoTypes: this.types, + pagination: Object.assign({}, this.defaultPagination, { + currentPage: page + }) + }) + ).pipe( + getFirstSucceededRemoteDataPayload() + ); + } + + /** + * When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page + */ + onScrollDown() { + if (this.hasNextPage) { + this.currentPage$.next(this.currentPage$.value + 1); + } } /** @@ -126,4 +192,11 @@ export class DSOSelectorComponent implements OnInit { this.listElements.first.nativeElement.click(); } } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } } diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html index ef8865ad87..a188b08b60 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -5,7 +5,6 @@ diff --git a/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..8439ca53f7 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; +import { Collection } from '../../../../core/shared/collection.model'; +import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../core/shared/context.model'; +import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component'; + +@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-collection-sidebar-search-list-element', + templateUrl: '../sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link CollectionSearchResult} within the context of a sidebar search modal + */ +export class CollectionSidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the description of the Collection by returning its abstract + */ + getDescription(): string { + return this.firstMetadataValue('dc.description.abstract'); + } +} diff --git a/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts new file mode 100644 index 0000000000..02e09c3fd4 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../core/shared/context.model'; +import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { SidebarSearchListElementComponent } from '../sidebar-search-list-element.component'; +import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; +import { Community } from '../../../../core/shared/community.model'; + +@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.SideBarSearchModal) +@Component({ + selector: 'ds-collection-sidebar-search-list-element', + templateUrl: '../sidebar-search-list-element.component.html' +}) +/** + * Component displaying a list element for a {@link CommunitySearchResult} within the context of a sidebar search modal + */ +export class CommunitySidebarSearchListElementComponent extends SidebarSearchListElementComponent { + /** + * Get the description of the Community by returning its abstract + */ + getDescription(): string { + return this.firstMetadataValue('dc.description.abstract'); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 5b849eb403..d8d8b51331 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -212,6 +212,9 @@ import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-tr import { CurationFormComponent } from '../curation-form/curation-form.component'; import { PublicationSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component'; import { SidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/sidebar-search-list-element.component'; +import { CollectionSidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component'; +import { CommunitySidebarSearchListElementComponent } from './object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component'; +import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -405,7 +408,8 @@ const COMPONENTS = [ CollectionDropdownComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, - VocabularyTreeviewComponent + VocabularyTreeviewComponent, + AuthorizedCollectionSelectorComponent, ]; const ENTRY_COMPONENTS = [ @@ -488,6 +492,9 @@ const ENTRY_COMPONENTS = [ VocabularyTreeviewComponent, SidebarSearchListElementComponent, PublicationSidebarSearchListElementComponent, + CollectionSidebarSearchListElementComponent, + CommunitySidebarSearchListElementComponent, + AuthorizedCollectionSelectorComponent, ]; const SHARED_ITEM_PAGE_COMPONENTS = [ diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index c1f155fa39..bc1dfda7e7 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -37,3 +37,5 @@ $edit-item-metadata-field-width: 190px !default; $edit-item-language-field-width: 43px !default; $thumbnail-max-width: 175px !default; + +$dso-selector-list-max-height: 475px !default; From 1127b363e90ba4377cab130d8cdea42c75d3699a Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 15:45:39 +0200 Subject: [PATCH 11/78] [CSTPER-66] function to get succeeded or no content response --- src/app/core/shared/operators.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ad2588f2b9..203f66b074 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -75,6 +75,11 @@ export const getSucceededRemoteWithNotEmptyData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded && isNotEmpty(rd.payload))); +export const getSucceededOrNoContentResponse = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => rd.hasSucceeded || rd.hasNoContent)); + + /** * Get the first successful remotely retrieved object * From e1cce311e8a24f1183c377104fd6c31017ebf441 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 15:46:05 +0200 Subject: [PATCH 12/78] [CSTPER-66] utility method to check if remote data has no content --- src/app/core/data/remote-data.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index 8502c8ba1d..17fbe6a8a6 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -55,4 +55,8 @@ export class RemoteData { return this.state === RemoteDataState.Success; } + get hasNoContent(): boolean { + return this.statusCode === 204; + } + } From 20c7afce06532c033552f69e35fdb183c2aac091 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 15:46:29 +0200 Subject: [PATCH 13/78] [CSTPER-66] utility functions to create remote data object without content --- src/app/shared/remote-data.utils.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/app/shared/remote-data.utils.ts b/src/app/shared/remote-data.utils.ts index ce278e0229..580b68673b 100644 --- a/src/app/shared/remote-data.utils.ts +++ b/src/app/shared/remote-data.utils.ts @@ -69,3 +69,24 @@ export function createPendingRemoteDataObject(object?: T): RemoteData { export function createPendingRemoteDataObject$(object?: T): Observable> { return observableOf(createPendingRemoteDataObject(object)); } + +/** + * Method to create a remote data object with no content + */ +export function createNoContentRemoteDataObject(): RemoteData { + return new RemoteData( + true, + true, + true, + null, + null, + 204 + ); +} + +/** + * Method to create a remote data object that has succeeded with no content, wrapped in an observable + */ +export function createNoContentRemoteDataObject$(): Observable> { + return observableOf(createNoContentRemoteDataObject()); +} From be6a6a2f62cccd34d641882840103302dd99879c Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 15:50:44 +0200 Subject: [PATCH 14/78] [CSTPER-66] when a community or a collection is created or deleted, angular cache is refreshed so that in case of new community or collection it appears immediately in pages where communities and collections lists are displayed, in case of community or collection deletion deleted community or collection does not appear anymore in pages where communities and collections lists are displayed. --- .../create-comcol-page.component.spec.ts | 108 +++++++++++++++++- .../create-comcol-page.component.ts | 14 +-- .../delete-comcol-page.component.spec.ts | 107 ++++++++++++++++- .../delete-comcol-page.component.ts | 21 ++-- 4 files changed, 226 insertions(+), 24 deletions(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 780589d0c5..d2335f623f 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { RouteService } from '../../../core/services/route.service'; import { Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { Community } from '../../../core/shared/community.model'; import { SharedModule } from '../../shared.module'; @@ -12,12 +12,14 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { CreateComColPageComponent } from './create-comcol-page.component'; import { - createFailedRemoteDataObject$, + createFailedRemoteDataObject$, createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { ComColDataService } from '../../../core/data/comcol-data.service'; import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { RequestService } from '../../../core/data/request.service'; +import {getTestScheduler} from 'jasmine-marbles'; describe('CreateComColPageComponent', () => { let comp: CreateComColPageComponent; @@ -29,9 +31,11 @@ describe('CreateComColPageComponent', () => { let community; let newCommunity; + let parentCommunity; let communityDataServiceStub; let routeServiceStub; let routerStub; + let requestServiceStub; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -44,6 +48,15 @@ describe('CreateComColPageComponent', () => { }] }); + parentCommunity = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66as300d399', + id: 'a20da287-e174-466a-9926-f66as300d399', + metadata: [{ + key: 'dc.title', + value: 'parent community' + }] + }); + newCommunity = Object.assign(new Community(), { uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', metadata: [{ @@ -61,7 +74,8 @@ describe('CreateComColPageComponent', () => { }] })), create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), - getLogoEndpoint: () => observableOf(logoEndpoint) + getLogoEndpoint: () => observableOf(logoEndpoint), + findByHref: () => null }; routeServiceStub = { @@ -71,6 +85,10 @@ describe('CreateComColPageComponent', () => { navigate: (commands) => commands }; + requestServiceStub = jasmine.createSpyObj('RequestService', { + removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring'), + }); + } beforeEach(async(() => { @@ -82,7 +100,9 @@ describe('CreateComColPageComponent', () => { { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: TranslateService, useValue: {}}, + { provide: RequestService, useValue: requestServiceStub} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -123,19 +143,23 @@ describe('CreateComColPageComponent', () => { }; }); - it('should navigate when successful', () => { + it('should navigate and refresh cache when successful', () => { spyOn(router, 'navigate'); + spyOn((comp as any), 'refreshCache'); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).toHaveBeenCalled(); + expect((comp as any).refreshCache).toHaveBeenCalled(); }); - it('should not navigate on failure', () => { + it('should neither navigate nor refresh cache on failure', () => { spyOn(router, 'navigate'); spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity)); + spyOn((comp as any), 'refreshCache') comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); + expect((comp as any).refreshCache).not.toHaveBeenCalled(); }); }); @@ -182,5 +206,77 @@ describe('CreateComColPageComponent', () => { expect(data.uploader.uploadAll).toHaveBeenCalled(); }); }); + + describe('cache refresh', () => { + let scheduler; + let communityWithoutParentHref; + + beforeEach(() => { + scheduler = getTestScheduler(); + + }) + describe('cache refreshed top level community', () => { + beforeEach(() => { + spyOn(dsoDataService, 'findByHref').and.returnValue(createNoContentRemoteDataObject$()); + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'top level community' + }] + }), + _links: { + parentCommunity: { + href: 'topLevel/parentCommunity' + } + } + }; + communityWithoutParentHref = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'top level community' + }] + }), + _links: {} + }; + }); + it('top level community cache refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(data)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalledWith('communities/search/top'); + }); + it('top level community without parent link, cache not refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(communityWithoutParentHref)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).not.toHaveBeenCalled(); + }); + }); + + describe('cache refreshed child community', () => { + beforeEach(() => { + spyOn(dsoDataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity)); + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'child community' + }] + }), + _links: { + parentCommunity: { + href: 'child/parentCommunity' + } + } + }; + }); + it('child level community cache refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(data)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399'); + }); + }); + }); + }); }); diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index f55a7f0156..336998aab3 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -11,8 +11,7 @@ import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getFirstSucceededRemoteDataPayload, - getRemoteDataPayload, - getSucceededRemoteData + getSucceededOrNoContentResponse, } from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; import {hasValue, isEmpty, isNotEmpty, isNotUndefined} from '../../empty.util'; @@ -123,11 +122,12 @@ export class CreateComColPageComponent implements return; } this.dsoDataService.findByHref(parentCommunityUrl).pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((pc: TDomain) => isEmpty(pc) ? 'communities/search/top' : pc.id), - take(1) - ).subscribe((href: string) => this.requestService.removeByHrefSubstring(href)); + getSucceededOrNoContentResponse(), + take(1), + ).subscribe((rd: RemoteData) => { + const href = rd.hasSucceeded && !isEmpty(rd.payload.id) ? rd.payload.id : 'communities/search/top'; + this.requestService.removeByHrefSubstring(href) + }); } private parentCommunityUrl(dso: Collection | Community) { diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts index e791f41d56..9da77df007 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -1,7 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; +import {TranslateModule, TranslateService} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { Community } from '../../../core/shared/community.model'; import { SharedModule } from '../../shared.module'; @@ -13,6 +13,9 @@ import { DataService } from '../../../core/data/data.service'; import { DeleteComColPageComponent } from './delete-comcol-page.component'; import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import {RequestService} from '../../../core/data/request.service'; +import {getTestScheduler} from 'jasmine-marbles'; +import {createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../../remote-data.utils'; describe('DeleteComColPageComponent', () => { let comp: DeleteComColPageComponent; @@ -22,9 +25,13 @@ describe('DeleteComColPageComponent', () => { let community; let newCommunity; + let parentCommunity; let routerStub; let routeStub; let notificationsService; + let translateServiceStub; + let requestServiceStub; + const validUUID = 'valid-uuid'; const invalidUUID = 'invalid-uuid'; const frontendURL = '/testType'; @@ -45,10 +52,20 @@ describe('DeleteComColPageComponent', () => { }] }); + parentCommunity = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66as300d399', + id: 'a20da287-e174-466a-9926-f66as300d399', + metadata: [{ + key: 'dc.title', + value: 'parent community' + }] + }); + dsoDataService = jasmine.createSpyObj( 'dsoDataService', { - delete: observableOf({ isSuccessful: true }) + delete: observableOf({ isSuccessful: true }), + findByHref: jasmine.createSpy('findByHref') }); routerStub = { @@ -59,6 +76,14 @@ describe('DeleteComColPageComponent', () => { data: observableOf(community) }; + requestServiceStub = jasmine.createSpyObj('RequestService', { + removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring') + }); + + translateServiceStub = jasmine.createSpyObj('TranslateService', { + instant: jasmine.createSpy('instant') + }); + } beforeEach(async(() => { @@ -70,6 +95,8 @@ describe('DeleteComColPageComponent', () => { { provide: Router, useValue: routerStub }, { provide: ActivatedRoute, useValue: routeStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: TranslateService, useValue: translateServiceStub}, + { provide: RequestService, useValue: requestServiceStub} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -108,17 +135,21 @@ describe('DeleteComColPageComponent', () => { it('should show an error notification on failure', () => { (dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false })); spyOn(router, 'navigate'); + spyOn((comp as any), 'refreshCache'); comp.onConfirm(data2); fixture.detectChanges(); expect(notificationsService.error).toHaveBeenCalled(); + expect((comp as any).refreshCache).not.toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled(); }); it('should show a success notification on success and navigate', () => { spyOn(router, 'navigate'); + spyOn((comp as any), 'refreshCache'); comp.onConfirm(data1); fixture.detectChanges(); expect(notificationsService.success).toHaveBeenCalled(); + expect((comp as any).refreshCache).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled(); }); @@ -127,6 +158,78 @@ describe('DeleteComColPageComponent', () => { fixture.detectChanges(); expect(dsoDataService.delete).toHaveBeenCalledWith(data1.id); }); + + describe('cache refresh', () => { + let scheduler; + let communityWithoutParentHref; + let deletedCommunity; + + beforeEach(() => { + scheduler = getTestScheduler(); + + }) + describe('cache refreshed top level community', () => { + beforeEach(() => { + (dsoDataService.findByHref as any).and.returnValue(createNoContentRemoteDataObject$()); + deletedCommunity = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'top level community' + }] + }), + _links: { + parentCommunity: { + href: 'topLevel/parentCommunity' + } + } + }; + communityWithoutParentHref = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'top level community' + }] + }), + _links: {} + }; + }); + it('top level community cache refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(deletedCommunity)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalledWith('communities/search/top'); + }); + it('top level community without parent link, cache not refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(communityWithoutParentHref)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).not.toHaveBeenCalled(); + }); + }); + + describe('cache refreshed child community', () => { + beforeEach(() => { + (dsoDataService.findByHref as any).and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity)); + deletedCommunity = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'child community' + }] + }), + _links: { + parentCommunity: { + href: 'child/parentCommunity' + } + } + }; + }); + it('child level community cache refreshed', () => { + scheduler.schedule(() => (comp as any).refreshCache(deletedCommunity)); + scheduler.flush(); + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399'); + }); + }); + }); }); describe('onCancel', () => { diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts index 29ec4f7b19..835d302de3 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts @@ -10,7 +10,9 @@ import {TranslateService} from '@ngx-translate/core'; import {RestResponse} from '../../../core/cache/response.models'; import {hasValue, isEmpty, isNotEmpty} from '../../empty.util'; import {RequestService} from '../../../core/data/request.service'; -import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators'; +import { + getSucceededOrNoContentResponse, +} from '../../../core/shared/operators'; import {Community} from '../../../core/shared/community.model'; import {Collection} from '../../../core/shared/collection.model'; @@ -74,16 +76,17 @@ export class DeleteComColPageComponent implements } private refreshCache(dso: TDomain) { - const parentCommunity = this.parentCommunityUrl(dso as any); - if (!hasValue(parentCommunity)) { + const parentCommunityUrl = this.parentCommunityUrl(dso as any); + if (!hasValue(parentCommunityUrl)) { return; } - this.dsoDataService.findByHref(parentCommunity).pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((pc: TDomain) => isEmpty(pc) ? 'communities/search/top' : pc.id ), - take(1) - ).subscribe((id: string) => this.requestService.removeByHrefSubstring(id)); + this.dsoDataService.findByHref(parentCommunityUrl).pipe( + getSucceededOrNoContentResponse(), + take(1), + ).subscribe((rd: RemoteData) => { + const href = rd.hasSucceeded && !isEmpty(rd.payload.id) ? rd.payload.id : 'communities/search/top'; + this.requestService.removeByHrefSubstring(href) + }); } private parentCommunityUrl(dso: Collection | Community): string { From 5ef329f7a62d581a629d73351d1f0bf80c8588f8 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 16:44:49 +0200 Subject: [PATCH 15/78] [CSTPER-66] removed consecutive blank lines --- src/app/core/shared/operators.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 203f66b074..80c12612e0 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -79,7 +79,6 @@ export const getSucceededOrNoContentResponse = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded || rd.hasNoContent)); - /** * Get the first successful remotely retrieved object * From ade5eb53ccfa782c9be783bf1a6b3a9e960bcab4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 22 Oct 2020 18:03:32 +0200 Subject: [PATCH 16/78] 74199: Infinite scroll size changes --- .../authorized-collection-selector.component.ts | 1 + .../dso-selector/dso-selector/dso-selector.component.html | 2 +- .../shared/dso-selector/dso-selector/dso-selector.component.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts index ff9fa81368..1e58289c3a 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -12,6 +12,7 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; @Component({ selector: 'ds-authorized-collection-selector', + styleUrls: ['../dso-selector.component.scss'], templateUrl: '../dso-selector.component.html' }) /** diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index a0b6aff2a3..7a04bfc333 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -9,7 +9,7 @@
Date: Thu, 22 Oct 2020 20:15:08 +0200 Subject: [PATCH 17/78] [CSTPER-66] provided RequestService in constructor and parent community empty link --- .../create-collection-page.component.spec.ts | 4 +++- .../delete-community-page.component.spec.ts | 2 ++ .../create-comcol-page/create-comcol-page.component.spec.ts | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index d64e510a23..13047593cd 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCollectionPageComponent } from './create-collection-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import {RequestService} from '../../core/data/request.service'; describe('CreateCollectionPageComponent', () => { let comp: CreateCollectionPageComponent; @@ -29,7 +30,8 @@ describe('CreateCollectionPageComponent', () => { }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: RequestService, useValue: {}} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts index 613be9deb3..6c5f29b4b5 100644 --- a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts @@ -9,6 +9,7 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { SharedModule } from '../../shared/shared.module'; import { DeleteCommunityPageComponent } from './delete-community-page.component'; +import {RequestService} from '../../core/data/request.service'; describe('DeleteCommunityPageComponent', () => { let comp: DeleteCommunityPageComponent; @@ -22,6 +23,7 @@ describe('DeleteCommunityPageComponent', () => { { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: NotificationsService, useValue: {} }, + { provide: RequestService, useValue: {}} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index d2335f623f..bdd00a3a49 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -131,6 +131,7 @@ describe('CreateComColPageComponent', () => { value: 'test' }] }), + _links: {}, uploader: { options: { url: '' From d50d1506efa48e615268b21477c89c8aa0ea2051 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Thu, 22 Oct 2020 23:41:18 +0200 Subject: [PATCH 18/78] [CSTPER-66] provided RequestService --- .../delete-collection-page.component.spec.ts | 2 ++ .../create-community-page.component.spec.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts index d64c1d1915..efe7e99550 100644 --- a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts +++ b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts @@ -9,6 +9,7 @@ import { of as observableOf } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DeleteCollectionPageComponent } from './delete-collection-page.component'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import {RequestService} from '../../core/data/request.service'; describe('DeleteCollectionPageComponent', () => { let comp: DeleteCollectionPageComponent; @@ -22,6 +23,7 @@ describe('DeleteCollectionPageComponent', () => { { provide: CollectionDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: NotificationsService, useValue: {} }, + { provide: RequestService, useValue: {} } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index b4da0be4d1..c48e9158fb 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCommunityPageComponent } from './create-community-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import {RequestService} from '../../core/data/request.service'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; @@ -25,7 +26,8 @@ describe('CreateCommunityPageComponent', () => { { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: RequestService, useValue: {} } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); From 564e4e0b02ac4887a277f3abf039c0d66fb0ad68 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Fri, 23 Oct 2020 11:53:35 +0200 Subject: [PATCH 19/78] [CSTPER-66] onSubmit test executed within a scheduler and provided _links section on tested data --- .../create-comcol-page.component.spec.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index bdd00a3a49..898727b813 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -36,6 +36,7 @@ describe('CreateComColPageComponent', () => { let routeServiceStub; let routerStub; let requestServiceStub; + let scheduler; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -117,6 +118,7 @@ describe('CreateComColPageComponent', () => { communityDataService = (comp as any).communityDataService; routeService = (comp as any).routeService; router = (comp as any).router; + scheduler = getTestScheduler(); }); describe('onSubmit', () => { @@ -146,8 +148,9 @@ describe('CreateComColPageComponent', () => { it('should navigate and refresh cache when successful', () => { spyOn(router, 'navigate'); - spyOn((comp as any), 'refreshCache'); - comp.onSubmit(data); + spyOn((comp as any), 'refreshCache') + scheduler.schedule(() => comp.onSubmit(data)); + scheduler.flush(); fixture.detectChanges(); expect(router.navigate).toHaveBeenCalled(); expect((comp as any).refreshCache).toHaveBeenCalled(); @@ -157,7 +160,8 @@ describe('CreateComColPageComponent', () => { spyOn(router, 'navigate'); spyOn(dsoDataService, 'create').and.returnValue(createFailedRemoteDataObject$(newCommunity)); spyOn((comp as any), 'refreshCache') - comp.onSubmit(data); + scheduler.schedule(() => comp.onSubmit(data)); + scheduler.flush(); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); expect((comp as any).refreshCache).not.toHaveBeenCalled(); @@ -173,6 +177,7 @@ describe('CreateComColPageComponent', () => { value: 'test' }] }), + _links: {}, uploader: { options: { url: '' @@ -189,27 +194,29 @@ describe('CreateComColPageComponent', () => { it('should not navigate', () => { spyOn(router, 'navigate'); - comp.onSubmit(data); + scheduler.schedule(() => comp.onSubmit(data)); + scheduler.flush(); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); }); it('should set the uploader\'s url to the logo\'s endpoint', () => { - comp.onSubmit(data); + scheduler.schedule(() => comp.onSubmit(data)); + scheduler.flush(); fixture.detectChanges(); expect(data.uploader.options.url).toEqual(logoEndpoint); }); it('should call the uploader\'s uploadAll', () => { spyOn(data.uploader, 'uploadAll'); - comp.onSubmit(data); + scheduler.schedule(() => comp.onSubmit(data)); + scheduler.flush(); fixture.detectChanges(); expect(data.uploader.uploadAll).toHaveBeenCalled(); }); }); describe('cache refresh', () => { - let scheduler; let communityWithoutParentHref; beforeEach(() => { From 4bed16af3297fd6ce394b442d11c4066ae8d05c9 Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Fri, 23 Oct 2020 11:54:07 +0200 Subject: [PATCH 20/78] [CSTPER-66] onConfirm tests executed within a scheduler and provided _links section on tested data --- .../delete-comcol-page.component.spec.ts | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts index 9da77df007..b5a3ccea5d 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -32,6 +32,8 @@ describe('DeleteComColPageComponent', () => { let translateServiceStub; let requestServiceStub; + let scheduler; + const validUUID = 'valid-uuid'; const invalidUUID = 'invalid-uuid'; const frontendURL = '/testType'; @@ -109,34 +111,51 @@ describe('DeleteComColPageComponent', () => { notificationsService = (comp as any).notifications; (comp as any).frontendURL = frontendURL; router = (comp as any).router; + scheduler = getTestScheduler(); }); describe('onConfirm', () => { let data1; let data2; beforeEach(() => { - data1 = Object.assign(new Community(), { - uuid: validUUID, - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }); + data1 = { + dso: Object.assign(new Community(), { + uuid: validUUID, + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + _links: {} + }; - data2 = Object.assign(new Community(), { - uuid: invalidUUID, - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }); + data2 = { + dso: Object.assign(new Community(), { + uuid: invalidUUID, + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + _links: {}, + uploader: { + options: { + url: '' + }, + queue: [], + /* tslint:disable:no-empty */ + uploadAll: () => {} + /* tslint:enable:no-empty */ + } + }; }); it('should show an error notification on failure', () => { (dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false })); spyOn(router, 'navigate'); spyOn((comp as any), 'refreshCache'); - comp.onConfirm(data2); + scheduler.schedule(() => comp.onConfirm(data2)); + scheduler.flush(); fixture.detectChanges(); expect(notificationsService.error).toHaveBeenCalled(); expect((comp as any).refreshCache).not.toHaveBeenCalled(); @@ -146,7 +165,8 @@ describe('DeleteComColPageComponent', () => { it('should show a success notification on success and navigate', () => { spyOn(router, 'navigate'); spyOn((comp as any), 'refreshCache'); - comp.onConfirm(data1); + scheduler.schedule(() => comp.onConfirm(data1)); + scheduler.flush(); fixture.detectChanges(); expect(notificationsService.success).toHaveBeenCalled(); expect((comp as any).refreshCache).toHaveBeenCalled(); @@ -160,14 +180,9 @@ describe('DeleteComColPageComponent', () => { }); describe('cache refresh', () => { - let scheduler; let communityWithoutParentHref; let deletedCommunity; - beforeEach(() => { - scheduler = getTestScheduler(); - - }) describe('cache refreshed top level community', () => { beforeEach(() => { (dsoDataService.findByHref as any).and.returnValue(createNoContentRemoteDataObject$()); From 9658da4fc01d24f2f7da9f979b3f668ee058d69e Mon Sep 17 00:00:00 2001 From: Corrado Lombardi Date: Fri, 23 Oct 2020 16:40:49 +0200 Subject: [PATCH 21/78] [CSTPER-66] removed detectChanges in single tests, removed TranslateService stub provision, added _links section in community objects --- .../create-comcol-page.component.spec.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 898727b813..a7496fbf7a 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -46,7 +46,8 @@ describe('CreateComColPageComponent', () => { metadata: [{ key: 'dc.title', value: 'test community' - }] + }], + _links: {} }); parentCommunity = Object.assign(new Community(), { @@ -55,7 +56,8 @@ describe('CreateComColPageComponent', () => { metadata: [{ key: 'dc.title', value: 'parent community' - }] + }], + _links: {} }); newCommunity = Object.assign(new Community(), { @@ -63,7 +65,8 @@ describe('CreateComColPageComponent', () => { metadata: [{ key: 'dc.title', value: 'new community' - }] + }], + _links: {} }); communityDataServiceStub = { @@ -102,7 +105,6 @@ describe('CreateComColPageComponent', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, - { provide: TranslateService, useValue: {}}, { provide: RequestService, useValue: requestServiceStub} ], schemas: [NO_ERRORS_SCHEMA] @@ -151,7 +153,6 @@ describe('CreateComColPageComponent', () => { spyOn((comp as any), 'refreshCache') scheduler.schedule(() => comp.onSubmit(data)); scheduler.flush(); - fixture.detectChanges(); expect(router.navigate).toHaveBeenCalled(); expect((comp as any).refreshCache).toHaveBeenCalled(); }); @@ -162,7 +163,6 @@ describe('CreateComColPageComponent', () => { spyOn((comp as any), 'refreshCache') scheduler.schedule(() => comp.onSubmit(data)); scheduler.flush(); - fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); expect((comp as any).refreshCache).not.toHaveBeenCalled(); }); @@ -196,14 +196,12 @@ describe('CreateComColPageComponent', () => { spyOn(router, 'navigate'); scheduler.schedule(() => comp.onSubmit(data)); scheduler.flush(); - fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); }); it('should set the uploader\'s url to the logo\'s endpoint', () => { scheduler.schedule(() => comp.onSubmit(data)); scheduler.flush(); - fixture.detectChanges(); expect(data.uploader.options.url).toEqual(logoEndpoint); }); @@ -211,7 +209,6 @@ describe('CreateComColPageComponent', () => { spyOn(data.uploader, 'uploadAll'); scheduler.schedule(() => comp.onSubmit(data)); scheduler.flush(); - fixture.detectChanges(); expect(data.uploader.uploadAll).toHaveBeenCalled(); }); }); From 40891a3c35e89b416ba6507d4833bda728711084 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 23 Oct 2020 17:29:10 +0200 Subject: [PATCH 22/78] 74199: Admin search dialogs - Current DSO + context for new item/collection --- .../dso-selector/dso-selector.component.html | 1 + .../dso-selector/dso-selector.component.scss | 7 ++++ .../dso-selector/dso-selector.component.ts | 33 +++++++++++++++---- ...te-collection-parent-selector.component.ts | 1 + ...create-item-parent-selector.component.html | 3 +- .../create-item-parent-selector.component.ts | 1 + .../dso-selector-modal-wrapper.component.html | 3 +- .../dso-selector-modal-wrapper.component.ts | 6 ++++ src/assets/i18n/en.json5 | 4 +++ src/styles/_custom_variables.scss | 2 ++ 10 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 7a04bfc333..048e115f8e 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -20,6 +20,7 @@
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 5729ac8460..03d7732fb0 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 @@ -20,6 +20,7 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo objectType = DSpaceObjectType.ITEM; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.CREATE; + header = 'dso-selector.create.item.sub-level'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html index e1c18ec1e0..85d8797e66 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -5,6 +5,7 @@ diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index b56a901b12..59aeceea0f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -23,6 +23,12 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ @Input() dsoRD: RemoteData; + /** + * Optional header to display above the selection list + * Supports i18n keys + */ + @Input() header: string; + /** * The type of the DSO that's being edited, created or exported */ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a6aaad98ea..97d7ce162a 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1037,6 +1037,8 @@ "dso-selector.create.collection.head": "New collection", + "dso-selector.create.collection.sub-level": "Create a new collection in", + "dso-selector.create.community.head": "New community", "dso-selector.create.community.sub-level": "Create a new community in", @@ -1045,6 +1047,8 @@ "dso-selector.create.item.head": "New item", + "dso-selector.create.item.sub-level": "Create a new item in", + "dso-selector.create.submission.head": "New submission", "dso-selector.edit.collection.head": "Edit collection", diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index bc1dfda7e7..a9a1ecb391 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -39,3 +39,5 @@ $edit-item-language-field-width: 43px !default; $thumbnail-max-width: 175px !default; $dso-selector-list-max-height: 475px !default; +$dso-selector-current-background-color: #eeeeee; +$dso-selector-current-background-hover-color: darken($dso-selector-current-background-color, 10%); From 892e7bdaa1cf4831f92432d47abae917458cf7e8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 26 Oct 2020 15:43:15 +0100 Subject: [PATCH 23/78] 74199: Tests --- ...ebar-search-list-element.component.spec.ts | 42 ++++++++++ ...ebar-search-list-element.component.spec.ts | 45 +++++++++++ ...ebar-search-list-element.component.spec.ts | 40 ++++++++++ ...ebar-search-list-element.component.spec.ts | 37 +++++++++ ...ebar-search-list-element.component.spec.ts | 45 +++++++++++ ...ebar-search-list-element.component.spec.ts | 32 ++++++++ ...ized-collection-selector.component.spec.ts | 56 +++++++++++++ .../dso-selector.component.spec.ts | 79 ++++++++++++++----- ...ebar-search-list-element.component.spec.ts | 37 +++++++++ ...ebar-search-list-element.component.spec.ts | 36 +++++++++ ...ebar-search-list-element.component.spec.ts | 47 +++++++++++ ...sidebar-search-list-element.component.html | 6 +- ...ebar-search-list-element.component.spec.ts | 69 ++++++++++++++++ 13 files changed, 547 insertions(+), 24 deletions(-) create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.spec.ts create mode 100644 src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..b9593e7612 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,42 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { JournalIssueSidebarSearchListElementComponent } from './journal-issue-sidebar-search-list-element.component'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'publicationvolume.volumeNumber': [ + { + value: '5' + } + ], + 'publicationissue.issueNumber': [ + { + value: '7' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('JournalIssueSidebarSearchListElementComponent', + createSidebarSearchListElementTests(JournalIssueSidebarSearchListElementComponent, object, parent, 'parent title', 'title', '5 - 7') +); diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..eca1775c7d --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal-volume/journal-volume-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,45 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { JournalVolumeSidebarSearchListElementComponent } from './journal-volume-sidebar-search-list-element.component'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'journal.title': [ + { + value: 'journal title' + } + ], + 'publicationvolume.volumeNumber': [ + { + value: '1' + }, + { + value: '2' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('JournalVolumeSidebarSearchListElementComponent', + createSidebarSearchListElementTests(JournalVolumeSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'journal title (1) (2)') +); diff --git a/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..a47dbf0e2e --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,40 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { JournalSidebarSearchListElementComponent } from './journal-sidebar-search-list-element.component'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'creativeworkseries.issn': [ + { + value: '1234' + }, + { + value: '5678' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('JournalSidebarSearchListElementComponent', + createSidebarSearchListElementTests(JournalSidebarSearchListElementComponent, object, parent, 'parent title', 'title', '1234, 5678') +); diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..a271273a1b --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/org-unit/org-unit-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,37 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { OrgUnitSidebarSearchListElementComponent } from './org-unit-sidebar-search-list-element.component'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'organization.legalName': [ + { + value: 'title' + } + ], + 'dc.description': [ + { + value: 'description' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('OrgUnitSidebarSearchListElementComponent', + createSidebarSearchListElementTests(OrgUnitSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description') +); diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..e93dd78636 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,45 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { PersonSidebarSearchListElementComponent } from './person-sidebar-search-list-element.component'; +import { TranslateService } from '@ngx-translate/core'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'person.familyName': [ + { + value: 'family name' + } + ], + 'person.givenName': [ + { + value: 'given name' + } + ], + 'person.jobTitle': [ + { + value: 'job title' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('PersonSidebarSearchListElementComponent', + createSidebarSearchListElementTests(PersonSidebarSearchListElementComponent, object, parent, 'parent title', 'family name, given name', 'job title', [ + { provide: TranslateService, useValue: jasmine.createSpyObj('translate', { instant: '' }) } + ]) +); diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..ec0eb0e7bb --- /dev/null +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,32 @@ +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { createSidebarSearchListElementTests } from '../../../../../shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec'; +import { ProjectSidebarSearchListElementComponent } from './project-sidebar-search-list-element.component'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('ProjectSidebarSearchListElementComponent', + createSidebarSearchListElementTests(ProjectSidebarSearchListElementComponent, object, parent, 'parent title', 'title', undefined) +); diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts new file mode 100644 index 0000000000..25b7465ada --- /dev/null +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts @@ -0,0 +1,56 @@ +import { AuthorizedCollectionSelectorComponent } from './authorized-collection-selector.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 { SearchService } from '../../../../core/shared/search/search.service'; +import { CollectionDataService } from '../../../../core/data/collection-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createPaginatedList } from '../../../testing/utils.test'; +import { Collection } from '../../../../core/shared/collection.model'; +import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; + +describe('AuthorizedCollectionSelectorComponent', () => { + let component: AuthorizedCollectionSelectorComponent; + let fixture: ComponentFixture; + + let collectionService; + let collection; + + beforeEach(async(() => { + collection = Object.assign(new Collection(), { + id: 'authorized-collection' + }); + collectionService = jasmine.createSpyObj('collectionService', { + getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])) + }); + TestBed.configureTestingModule({ + declarations: [AuthorizedCollectionSelectorComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: SearchService, useValue: {} }, + { provide: CollectionDataService, useValue: collectionService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AuthorizedCollectionSelectorComponent); + component = fixture.componentInstance; + component.types = [DSpaceObjectType.COLLECTION]; + fixture.detectChanges(); + }); + + describe('search', () => { + it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => { + component.search('', 1).subscribe((result) => { + expect(collectionService.getAuthorizedCollection).toHaveBeenCalled(); + expect(result.page.length).toEqual(1); + expect(result.page[0].indexableObject).toEqual(collection); + done(); + }); + }); + }); +}); diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 50b6090aef..7671f012ad 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -6,10 +6,10 @@ import { SearchService } from '../../../core/shared/search/search.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { Item } from '../../../core/shared/item.model'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { MetadataValue } from '../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; +import { hasValue } from '../../empty.util'; +import { createPaginatedList } from '../../testing/utils.test'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; @@ -18,19 +18,46 @@ describe('DSOSelectorComponent', () => { const currentDSOId = 'test-uuid-ford-sose'; const type = DSpaceObjectType.ITEM; - const searchResult = new ItemSearchResult(); - const item = new Item(); - item.metadata = { - 'dc.title': [Object.assign(new MetadataValue(), { - value: 'Item title', - language: undefined - })] - }; - searchResult.indexableObject = item; - searchResult.hitHighlights = {}; - const searchService = jasmine.createSpyObj('searchService', { - search: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [searchResult])) - }); + const searchResult = createSearchResult('current'); + + const firstPageResults = [ + createSearchResult('1'), + createSearchResult('2'), + createSearchResult('3'), + ]; + + const nextPageResults = [ + createSearchResult('4'), + createSearchResult('5'), + createSearchResult('6'), + ]; + + const searchService = { + search: (options: PaginatedSearchOptions) => { + if (hasValue(options.query) && options.query.startsWith('search.resourceid')) { + return createSuccessfulRemoteDataObject$(createPaginatedList([searchResult])); + } else if (options.pagination.currentPage === 1) { + return createSuccessfulRemoteDataObject$(createPaginatedList(firstPageResults)); + } else { + return createSuccessfulRemoteDataObject$(createPaginatedList(nextPageResults)); + } + } + } + + function createSearchResult(name: string): ItemSearchResult { + return Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: `test-result-${name}`, + metadata: { + 'dc.title': [ + { + value: `test result - ${name}` + } + ] + } + }) + }) + } beforeEach(async(() => { TestBed.configureTestingModule({ @@ -58,13 +85,23 @@ describe('DSOSelectorComponent', () => { expect(component).toBeTruthy(); }); - it('should initially call the search method on the SearchService with the given DSO uuid', () => { - const searchOptions = new PaginatedSearchOptions({ - query: currentDSOId, - dsoTypes: [type], - pagination: (component as any).defaultPagination + describe('populating listEntries', () => { + it('should not be empty', () => { + expect(component.listEntries.length).toBeGreaterThan(0); }); - expect(searchService.search).toHaveBeenCalledWith(searchOptions); + it('should contain a combination of the current DSO and first page results', () => { + expect(component.listEntries).toEqual([searchResult, ...firstPageResults]); + }); + + describe('when current page increases', () => { + beforeEach(() => { + component.currentPage$.next(2); + }); + + it('should contain a combination of the current DSO, as well as first and second page results', () => { + expect(component.listEntries).toEqual([searchResult, ...firstPageResults, ...nextPageResults]); + }); + }); }); }); diff --git a/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..be3ee7d1bb --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/collection/collection-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,37 @@ +import { CollectionSidebarSearchListElementComponent } from './collection-sidebar-search-list-element.component'; +import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; +import { Collection } from '../../../../core/shared/collection.model'; +import { Community } from '../../../../core/shared/community.model'; +import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec'; + +const object = Object.assign(new CollectionSearchResult(), { + indexableObject: Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'dc.description.abstract': [ + { + value: 'description' + } + ] + } + }) +}); +const parent = Object.assign(new Community(), { + id: 'test-community', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('CollectionSidebarSearchListElementComponent', + createSidebarSearchListElementTests(CollectionSidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description') +); diff --git a/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..d6bcfc8581 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/community/community-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,36 @@ +import { Community } from '../../../../core/shared/community.model'; +import { createSidebarSearchListElementTests } from '../sidebar-search-list-element.component.spec'; +import { CommunitySidebarSearchListElementComponent } from './community-sidebar-search-list-element.component'; +import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; + +const object = Object.assign(new CommunitySearchResult(), { + indexableObject: Object.assign(new Community(), { + id: 'test-community', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'dc.description.abstract': [ + { + value: 'description' + } + ] + } + }) +}); +const parent = Object.assign(new Community(), { + id: 'test-parent-community', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('CommunitySidebarSearchListElementComponent', + createSidebarSearchListElementTests(CommunitySidebarSearchListElementComponent, object, parent, 'parent title', 'title', 'description') +); diff --git a/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..106289bba6 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/item-types/publication/publication-sidebar-search-list-element.component.spec.ts @@ -0,0 +1,47 @@ +import { createSidebarSearchListElementTests } from '../../sidebar-search-list-element.component.spec'; +import { PublicationSidebarSearchListElementComponent } from './publication-sidebar-search-list-element.component'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { Collection } from '../../../../../core/shared/collection.model'; + +const object = Object.assign(new ItemSearchResult(), { + indexableObject: Object.assign(new Item(), { + id: 'test-item', + metadata: { + 'dc.title': [ + { + value: 'title' + } + ], + 'dc.publisher': [ + { + value: 'publisher' + } + ], + 'dc.date.issued': [ + { + value: 'date' + } + ], + 'dc.contributor.author': [ + { + value: 'author' + } + ] + } + }) +}); +const parent = Object.assign(new Collection(), { + id: 'test-collection', + metadata: { + 'dc.title': [ + { + value: 'parent title' + } + ] + } +}); + +describe('PublicationSidebarSearchListElementComponent', + createSidebarSearchListElementTests(PublicationSidebarSearchListElementComponent, object, parent, 'parent title', 'title', '(publisher, date) author') +); diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html index adcf23dcf7..0bad828932 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.html @@ -1,3 +1,3 @@ -
{{ parentTitle$ | async }}
-
{{ title }}
-
{{ description }}
+
+
+
diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts new file mode 100644 index 0000000000..f7ec05df95 --- /dev/null +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts @@ -0,0 +1,69 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../../utils/var.directive'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SearchResult } from '../../search/search-result.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { TruncatableService } from '../../truncatable/truncatable.service'; +import { LinkService } from '../../../core/cache/builders/link.service'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { HALResource } from '../../../core/shared/hal-resource.model'; +import { ChildHALResource } from '../../../core/shared/child-hal-resource.model'; + +export function createSidebarSearchListElementTests( + componentClass: any, + object: SearchResult, + parent: DSpaceObject, + expectedParentTitle: string, + expectedTitle: string, + expectedDescription: string, + extraProviders: any[] = [] +) { + return () => { + let component; + let fixture: ComponentFixture; + + let linkService; + + beforeEach(async(() => { + linkService = jasmine.createSpyObj('linkService', { + resolveLink: Object.assign(new HALResource(), { + [object.indexableObject.getParentLinkKey()]: createSuccessfulRemoteDataObject$(parent) + }) + }); + TestBed.configureTestingModule({ + declarations: [componentClass, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: TruncatableService, useValue: {} }, + { provide: LinkService, useValue: linkService }, + ...extraProviders + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(componentClass); + component = fixture.componentInstance; + component.object = object; + fixture.detectChanges(); + }); + + it('should contain the correct parent title', (done) => { + component.parentTitle$.subscribe((title) => { + expect(title).toEqual(expectedParentTitle); + done(); + }); + }); + + it('should contain the correct title', () => { + expect(component.title).toEqual(expectedTitle); + }); + + it('should contain the correct description', () => { + expect(component.description).toEqual(expectedDescription); + }); + }; +} From 06769493e1c16f65f77fcad9afeab4acc0b09e7c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 26 Oct 2020 17:49:56 +0100 Subject: [PATCH 24/78] 74199: Edit current DSO button --- .../collection-page.component.html | 62 ++++++++++--------- .../community-page.component.html | 40 ++++++------ .../item-status/item-status.component.ts | 2 +- .../item-version-history.component.ts | 2 +- .../full/full-item-page.component.html | 7 ++- .../publication/publication.component.html | 11 +++- .../data/feature-authorization/feature-id.ts | 1 + .../journal-issue.component.html | 11 +++- .../journal-volume.component.html | 11 +++- .../item-pages/journal/journal.component.html | 11 +++- .../org-unit/org-unit.component.html | 11 +++- .../item-pages/person/person.component.html | 11 +++- .../item-pages/project/project.component.html | 11 +++- .../dso-page-edit-button.component.html | 6 ++ .../dso-page-edit-button.component.scss | 3 + .../dso-page-edit-button.component.spec.ts | 25 ++++++++ .../dso-page-edit-button.component.ts | 28 +++++++++ src/app/shared/shared.module.ts | 21 ++++--- src/assets/i18n/en.json5 | 20 ++++++ 19 files changed, 214 insertions(+), 80 deletions(-) create mode 100644 src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.html create mode 100644 src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss create mode 100644 src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.spec.ts create mode 100644 src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.ts diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 98552ed40b..beb7413415 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -3,37 +3,41 @@ *ngVar="(collectionRD$ | async) as collectionRD">
- -
+ +
+
- - - - - + + + + + - - - - - - - - - - -
+ + + + + + + + + +
+
+ +
+
-
- - - - - - - - - - - - - - - -
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index dd043330d6..3cd885ddca 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -56,7 +56,7 @@ export class ItemStatusComponent implements OnInit { } ngOnInit(): void { - this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)); + this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)); this.itemRD$.pipe( first(), map((data: RemoteData) => data.payload) diff --git a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts index ce662c5753..7cdd043603 100644 --- a/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts +++ b/src/app/+item-page/edit-item-page/item-version-history/item-version-history.component.ts @@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent { } ngOnInit(): void { - this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable>; + this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; } } diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index 29d3582492..ce63c269de 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -3,7 +3,12 @@
- +
+ +
+ +
+