From 6a8e20c2b96fc40b656a830a5c3fc2491699dd13 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 22 Nov 2023 11:17:22 +0100 Subject: [PATCH 01/31] [DSC-1352] Move breadcrumb char limit property inside layout configuration properties --- ...runcate-breadcrumb-item-characters.pipe.ts | 2 +- src/config/app-config.interface.ts | 1 - src/config/default-app-config.ts | 38 ++++++++++--------- src/config/layout-config.interfaces.ts | 5 +++ src/environments/environment.test.ts | 6 ++- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/app/breadcrumbs/breadcrumb/truncate-breadcrumb-item-characters.pipe.ts b/src/app/breadcrumbs/breadcrumb/truncate-breadcrumb-item-characters.pipe.ts index edd6bcdb4d5..ebfade6f89f 100644 --- a/src/app/breadcrumbs/breadcrumb/truncate-breadcrumb-item-characters.pipe.ts +++ b/src/app/breadcrumbs/breadcrumb/truncate-breadcrumb-item-characters.pipe.ts @@ -10,7 +10,7 @@ export class TruncateBreadcrumbItemCharactersPipe implements PipeTransform { * The maximum number of characters to display in a breadcrumb item * @type {number} */ - readonly charLimit: number = environment.breadcrumbCharLimit; + readonly charLimit: number = environment.layout.breadcrumbs.charLimit; /** * Truncates the text based on the configured char number allowed per breadcrumb element. diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 8d03ef86426..f95a82dd399 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -68,7 +68,6 @@ interface AppConfig extends Config { attachmentRendering: AttachmentRenderingConfig; advancedAttachmentRendering: AdvancedAttachmentRenderingConfig; searchResult: SearchResultConfig; - breadcrumbCharLimit: number; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 2b4a24e1043..bb4f5740a74 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -511,23 +511,23 @@ export class DefaultAppConfig implements AppConfig { urn: [ { name: 'doi', - baseUrl: 'https://doi.org/' + baseUrl: 'https://doi.org/', }, { name: 'hdl', - baseUrl: 'https://hdl.handle.net/' + baseUrl: 'https://hdl.handle.net/', }, { name: 'scopus', - baseUrl: 'https://www.scopus.com/authid/detail.uri?authorId=' + baseUrl: 'https://www.scopus.com/authid/detail.uri?authorId=', }, { name: 'researcherid', - baseUrl: 'http://www.researcherid.com/rid/' + baseUrl: 'http://www.researcherid.com/rid/', }, { name: 'mailto', - baseUrl: 'mailto:' + baseUrl: 'mailto:', } ], crisRef: [ @@ -536,7 +536,7 @@ export class DefaultAppConfig implements AppConfig { entityStyle: { default: { icon: 'fa fa-info', - style: 'text-info' + style: 'text-info', } } }, @@ -545,7 +545,7 @@ export class DefaultAppConfig implements AppConfig { entityStyle: { default: { icon: 'fa fa-user', - style: 'text-info' + style: 'text-info', } } }, @@ -554,7 +554,7 @@ export class DefaultAppConfig implements AppConfig { entityStyle: { default: { icon: 'fa fa-university', - style: 'text-info' + style: 'text-info', } } }, @@ -563,7 +563,7 @@ export class DefaultAppConfig implements AppConfig { entityStyle: { default: { icon: 'fas fa-project-diagram', - style: 'text-info' + style: 'text-info', } } } @@ -573,18 +573,18 @@ export class DefaultAppConfig implements AppConfig { }, itemPage: { OrgUnit: { - orientation: 'vertical' + orientation: 'vertical', }, Project: { - orientation: 'vertical' + orientation: 'vertical', }, default: { - orientation: 'horizontal' + orientation: 'horizontal', }, }, metadataBox: { defaultMetadataLabelColStyle: 'col-3', - defaultMetadataValueColStyle: 'col-9' + defaultMetadataValueColStyle: 'col-9', }, collectionsBox: { defaultCollectionsLabelColStyle: 'col-3 font-weight-bold', @@ -597,6 +597,9 @@ export class DefaultAppConfig implements AppConfig { navbar: { // If true, show the "Community and Collections" link in the navbar; otherwise, show it in the admin sidebar showCommunityCollection: true, + }, + breadcrumbs: { + charLimit: 10, } }; @@ -605,17 +608,17 @@ export class DefaultAppConfig implements AppConfig { { value: 0, icon: 'fa fa-globe', - color: 'green' + color: 'green', }, { value: 1, icon: 'fa fa-key', - color: 'orange' + color: 'orange', }, { value: 2, icon: 'fa fa-lock', - color: 'red' + color: 'red', } ] }; @@ -730,7 +733,7 @@ export class DefaultAppConfig implements AppConfig { name: 'checksum', type: AdvancedAttachmentElementType.Attribute, } - ] + ], }; searchResult: SearchResultConfig = { @@ -738,5 +741,4 @@ export class DefaultAppConfig implements AppConfig { authorMetadata: ['dc.contributor.author', 'dc.creator', 'dc.contributor.*'], }; - breadcrumbCharLimit = 10; } diff --git a/src/config/layout-config.interfaces.ts b/src/config/layout-config.interfaces.ts index b5f293800c1..88e52ac54fd 100644 --- a/src/config/layout-config.interfaces.ts +++ b/src/config/layout-config.interfaces.ts @@ -38,6 +38,10 @@ export interface NavbarConfig extends Config { showCommunityCollection: boolean; } +export interface BreadcrumbsConfig extends Config { + charLimit: number; +} + export interface CrisItemPageConfig extends Config { [entity: string]: CrisLayoutTypeConfig; default: CrisLayoutTypeConfig; @@ -59,6 +63,7 @@ export interface CrisLayoutConfig extends Config { export interface LayoutConfig extends Config { navbar: NavbarConfig; + breadcrumbs: BreadcrumbsConfig; } export interface SuggestionConfig extends Config { diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 6956db23dcb..ee4912c49a0 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -421,7 +421,10 @@ export const environment: BuildConfig = { navbar: { // If true, show the "Community and Collections" link in the navbar; otherwise, show it in the admin sidebar showCommunityCollection: true, - } + }, + breadcrumbs: { + charLimit: 10, + }, }, security: { levels: [ @@ -554,5 +557,4 @@ export const environment: BuildConfig = { authorMetadata: ['dc.contributor.author', 'dc.contributor.editor', 'dc.contributor.contributor', 'dc.creator'], }, - breadcrumbCharLimit: 10, }; From b513c5566e525dc47dac7ba6c188496d96565c2b Mon Sep 17 00:00:00 2001 From: Nikita Krivonosov Date: Thu, 30 Nov 2023 08:49:58 +0100 Subject: [PATCH 02/31] [DSC-1408] - Fix dropdown readonly field --- src/app/shared/form/form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index fa1fda949d9..6e50faebfa9 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -20,7 +20,7 @@ title="{{'form.remove' | translate}}" attr.aria-label="{{'form.remove' | translate}}" (click)="clearScrollableDropdown($event, model)" - [disabled]="!model.value"> + [disabled]="!model.value || model.readOnly"> From 4724dfcb1a11cdfd1af4c8228060b3c34be5109b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Nov 2023 17:53:15 +0100 Subject: [PATCH 03/31] Prepare next development iteration --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7bd0d51961b..7a3f9dff512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dspace-angular", - "version": "2023.02.00", + "version": "2023.02.01-SNAPSHOT", "scripts": { "ng": "ng", "config:watch": "nodemon", From fb31da728bcc4fa940b191051002fd98e3df0924 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 7 Dec 2023 17:57:38 +0100 Subject: [PATCH 04/31] [DSC-1037] Adds semicolon to author-list --- .../item-list-preview/item-list-preview.component.html | 4 +++- .../relationships-items-list-preview.component.html | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index 25bed065bb7..64ea279ed01 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -19,7 +19,9 @@

{{'mydspace.results.no-authors' | translate}} - + ; diff --git a/src/app/shared/object-list/relationships-list/relationships-items-list-preview/relationships-items-list-preview.component.html b/src/app/shared/object-list/relationships-list/relationships-items-list-preview/relationships-items-list-preview.component.html index 03f5ea0f99e..594793cd50b 100644 --- a/src/app/shared/object-list/relationships-list/relationships-items-list-preview/relationships-items-list-preview.component.html +++ b/src/app/shared/object-list/relationships-list/relationships-items-list-preview/relationships-items-list-preview.component.html @@ -24,7 +24,9 @@

{{'mydspace.results.no-authors' | translate}} From bb002aaadc31cc9c0acfc6ed63907318713a094d Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Mon, 18 Dec 2023 17:55:34 +0100 Subject: [PATCH 05/31] [DSC-1434] feature: update twitter to x button --- src/config/default-app-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index bb4f5740a74..06941787a91 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -642,7 +642,7 @@ export class DefaultAppConfig implements AppConfig { addToAnyPlugin: AddToAnyPluginConfig = { scriptUrl: 'https://static.addtoany.com/menu/page.js', socialNetworksEnabled: false, - buttons: ['facebook', 'twitter', 'linkedin', 'email', 'copy_link'], + buttons: ['facebook', 'x', 'linkedin', 'email', 'copy_link'], showPlusButton: true, showCounters: true, title: 'DSpace CRIS 7 demo', From 4540186b360238f3c35d2b8ecdea7bfcd91e062c Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 21 Dec 2023 16:38:04 +0100 Subject: [PATCH 06/31] [DSC-1434] fix configuration issue ('link' property removed) --- src/app/social/social.component.ts | 1 - src/app/social/social.service.ts | 8 -------- src/config/add-to-any-plugin-config.ts | 1 - src/config/default-app-config.ts | 2 -- 4 files changed, 12 deletions(-) diff --git a/src/app/social/social.component.ts b/src/app/social/social.component.ts index 0898a2c4f14..ff8c1128819 100644 --- a/src/app/social/social.component.ts +++ b/src/app/social/social.component.ts @@ -36,7 +36,6 @@ export class SocialComponent implements OnInit { this.showPlusButton = this.socialService.configuration.showPlusButton; this.showCounters = this.socialService.configuration.showCounters; this.title = this.socialService.configuration.title; - this.url = this.socialService.link; this.socialService.initializeAddToAnyScript(); this.showOnCurrentRoute$ = this.socialService.showOnCurrentRoute$; } diff --git a/src/app/social/social.service.ts b/src/app/social/social.service.ts index 308e1af1544..7994ac8c7dc 100644 --- a/src/app/social/social.service.ts +++ b/src/app/social/social.service.ts @@ -54,14 +54,6 @@ export class SocialService { return environment.addToAnyPlugin; } - /** - * Returns the homepage link to be used by AddToAny. - * If no link is provided in the configuration then the current page url will be used. - */ - get link(): string { - return environment.addToAnyPlugin.link ?? null; - } - /** * Import the AddToAny JavaScript */ diff --git a/src/config/add-to-any-plugin-config.ts b/src/config/add-to-any-plugin-config.ts index abe7d6e0c88..a4695f6ada8 100644 --- a/src/config/add-to-any-plugin-config.ts +++ b/src/config/add-to-any-plugin-config.ts @@ -8,5 +8,4 @@ export interface AddToAnyPluginConfig extends Config { buttons: string[]; showPlusButton: boolean; showCounters: boolean; - link?: string; } diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 06941787a91..d0706bd4398 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -646,8 +646,6 @@ export class DefaultAppConfig implements AppConfig { showPlusButton: true, showCounters: true, title: 'DSpace CRIS 7 demo', - // link: 'https://dspacecris7.4science.cloud/', - // The link to be shown in the shared post, if different from document.location.origin }; metricVisualizationConfig: MetricVisualizationConfig[] = [ From 4f03340dcc7dfa4b724b918d3e5377386f2f282b Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Wed, 27 Dec 2023 17:47:04 +0100 Subject: [PATCH 07/31] [DSC-1108] added unit tests on collection/community page --- .../collection-page.component.spec.ts | 144 ++++++++++++++++++ .../community-page.component.spec.ts | 123 +++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 src/app/collection-page/collection-page.component.spec.ts create mode 100644 src/app/community-page/community-page.component.spec.ts diff --git a/src/app/collection-page/collection-page.component.spec.ts b/src/app/collection-page/collection-page.component.spec.ts new file mode 100644 index 00000000000..15fce2cd9ac --- /dev/null +++ b/src/app/collection-page/collection-page.component.spec.ts @@ -0,0 +1,144 @@ +import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; +import { CollectionPageComponent } from './collection-page.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { CollectionDataService } from '../core/data/collection-data.service'; +import { AuthService } from '../core/auth/auth.service'; +import { PaginationService } from '../core/pagination/pagination.service'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { APP_CONFIG } from '../../../src/config/app-config.interface'; +import { PLATFORM_ID } from '@angular/core'; +import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { environment } from 'src/environments/environment.test'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { Collection } from '../core/shared/collection.model'; +import { SearchService } from '../core/shared/search/search.service'; +import { By } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../shared/utils/var.directive'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Bitstream } from '../core/shared/bitstream.model'; + +describe('CollectionPageComponent', () => { + let component: CollectionPageComponent; + let compAsAny: any; + let fixture: ComponentFixture; + + let collectionDataServiceSpy: jasmine.SpyObj; + let authServiceSpy: jasmine.SpyObj; + let paginationServiceSpy: jasmine.SpyObj; + let authorizationDataServiceSpy: jasmine.SpyObj; + let dsoNameServiceSpy: jasmine.SpyObj; + let searchServiceSpy: jasmine.SpyObj; + let aroute = new ActivatedRouteStub(); + let router = new RouterStub(); + + const collection = Object.assign(new Collection(), {}); + + beforeEach(async () => { + authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); + paginationServiceSpy = jasmine.createSpyObj('PaginationService', ['getCurrentPagination', 'getCurrentSort', 'clearPagination']); + authorizationDataServiceSpy = jasmine.createSpyObj('AuthorizationDataService', ['isAuthorized']); + collectionDataServiceSpy = jasmine.createSpyObj('CollectionDataService', ['findById', 'getAuthorizedCollection']); + searchServiceSpy = jasmine.createSpyObj('SearchService', ['search']); + dsoNameServiceSpy = jasmine.createSpyObj('DSONameService', ['getName']); + + await TestBed.configureTestingModule({ + imports: [RouterTestingModule, FormsModule, TranslateModule.forRoot(), BrowserAnimationsModule], + declarations: [CollectionPageComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: aroute }, + { provide: Router, useValue: router }, + { provide: CollectionDataService, useValue: collectionDataServiceSpy }, + { provide: AuthService, useValue: authServiceSpy }, + { provide: PaginationService, useValue: paginationServiceSpy }, + { provide: AuthorizationDataService, useValue: authorizationDataServiceSpy }, + { provide: DSONameService, useValue: dsoNameServiceSpy }, + { provide: SearchService, useValue: searchServiceSpy }, + { provide: APP_CONFIG, useValue: environment }, + { provide: PLATFORM_ID, useValue: 'browser' }, + ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionPageComponent); + component = fixture.componentInstance; + compAsAny = component as any; + component.collectionRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Collection(), { + name: 'Test Collection', + })); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize the component', () => { + const routeData = { + dso: createSuccessfulRemoteDataObject$(collection), + }; + Object.defineProperty(TestBed.inject(ActivatedRoute), 'data', { + get: () => of(routeData), + }); + authorizationDataServiceSpy.isAuthorized.and.returnValue(of(true)); + component.ngOnInit(); + + expect(component.collectionRD$).toBeDefined(); + expect(component.logoRD$).toBeDefined(); + expect(component.isCollectionAdmin$).toBeDefined(); + expect(compAsAny.paginationChanges$).toBeDefined(); + expect(component.itemRD$).toBeDefined(); + expect(component.collectionPageRoute$).toBeDefined(); + }); + + it('should display collection name', fakeAsync(() => { + component.collectionRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Collection(), { + name: 'Test Collection', + })); + fixture.detectChanges(); + fixture.whenStable().then(() => { + const collectionNameElement = fixture.debugElement.query(By.css('ds-comcol-page-header')).nativeElement; + expect(collectionNameElement.textContent.trim()).toBe('Test Collection'); + }); + })); + + it('should display collection logo if available', () => { + component.collectionRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Collection(), { + name: 'Test Collection', + })); + component.logoRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Bitstream(), { + name: 'Test Logo', + })); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')).nativeElement; + expect(logoElement).toBeTruthy(); + }); + }); + + it('should not display collection logo if not available', () => { + component.collectionRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Collection(), { + name: 'Test Collection', + })); + component.logoRD$ = of({ hasSucceeded: false, payload: null }) as any; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')); + expect(logoElement).toBeNull(); + }); + }); + + it('should clear pagination on ngOnDestroy', () => { + component.ngOnDestroy(); + expect(paginationServiceSpy.clearPagination).toHaveBeenCalledWith(component.paginationConfig.id); + }); +}); diff --git a/src/app/community-page/community-page.component.spec.ts b/src/app/community-page/community-page.component.spec.ts new file mode 100644 index 00000000000..d5f457fe24f --- /dev/null +++ b/src/app/community-page/community-page.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CommunityPageComponent } from './community-page.component'; +import { AuthService } from '../core/auth/auth.service'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FormsModule } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../shared/utils/var.directive'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { Community } from '../core/shared/community.model'; +import { of } from 'rxjs'; +import { CommunityDataService } from '../core/data/community-data.service'; +import { MetadataService } from '../core/metadata/metadata.service'; +import { Bitstream } from '../core/shared/bitstream.model'; +import { By } from '@angular/platform-browser'; + +describe('CommunityPageComponent', () => { + let component: CommunityPageComponent; + let fixture: ComponentFixture; + + let authServiceSpy: jasmine.SpyObj; + let authorizationDataServiceSpy: jasmine.SpyObj; + let dsoNameServiceSpy: jasmine.SpyObj; + let aroute = new ActivatedRouteStub(); + let router = new RouterStub(); + + const community = Object.assign(new Community(), { + id: 'test-community', + uuid: 'test-community', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'test community' + } + ], + logo: createSuccessfulRemoteDataObject$(new Bitstream()), + }); + + beforeEach(async () => { + authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); + authorizationDataServiceSpy = jasmine.createSpyObj('AuthorizationDataService', ['isAuthorized']); + dsoNameServiceSpy = jasmine.createSpyObj('DSONameService', ['getName']); + await TestBed.configureTestingModule({ + imports: [RouterTestingModule, FormsModule, TranslateModule.forRoot(), BrowserAnimationsModule], + declarations: [CommunityPageComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: aroute }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authServiceSpy }, + { provide: AuthorizationDataService, useValue: authorizationDataServiceSpy }, + { provide: DSONameService, useValue: dsoNameServiceSpy }, + { provide: CommunityDataService, useValue: {} }, + { provide: MetadataService, useValue: {} } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize the component', () => { + const routeData = { + data: of({ dso: createSuccessfulRemoteDataObject$(community) }), + }; + authorizationDataServiceSpy.isAuthorized.and.returnValue(of(true)); + + Object.defineProperty(TestBed.inject(ActivatedRoute), 'data', { + get: () => of(routeData), + }); + + component.ngOnInit(); + expect(component.communityRD$).toBeDefined(); + expect(component.logoRD$).toBeDefined(); + expect(component.communityPageRoute$).toBeDefined(); + expect(component.isCommunityAdmin$).toBeDefined(); + }); + + it('should display community logo if available', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(community); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')).nativeElement; + expect(logoElement).toBeTruthy(); + }); + }); + + + it('should not display community logo if not available', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Community(), { + name: 'Test', + logo: createSuccessfulRemoteDataObject$(null), + })); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')); + expect(logoElement).toBeNull(); + }); + }); + + it('should display collection name', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(Object.assign(community)); + fixture.detectChanges(); + fixture.whenStable().then(() => { + const collectionNameElement = fixture.debugElement.query(By.css('ds-comcol-page-header')).nativeElement; + expect(collectionNameElement.textContent.trim()).toBe('Test Collection'); + }); + }); +}); From 3456285076faaaee0245e8b659c420a0fc198986 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 29 Nov 2023 14:25:36 +0100 Subject: [PATCH 08/31] ensure HALEndpointService doesn't use stale requests --- src/app/core/server-check/server-check.guard.spec.ts | 10 ++++++---- src/app/core/server-check/server-check.guard.ts | 6 ++---- src/app/core/shared/hal-endpoint.service.ts | 12 ++++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/app/core/server-check/server-check.guard.spec.ts b/src/app/core/server-check/server-check.guard.spec.ts index 044609ef427..f65a7deca7c 100644 --- a/src/app/core/server-check/server-check.guard.spec.ts +++ b/src/app/core/server-check/server-check.guard.spec.ts @@ -9,7 +9,7 @@ import SpyObj = jasmine.SpyObj; describe('ServerCheckGuard', () => { let guard: ServerCheckGuard; let router: Router; - const eventSubject = new ReplaySubject(1); + let eventSubject: ReplaySubject; let rootDataServiceStub: SpyObj; let testScheduler: TestScheduler; let redirectUrlTree: UrlTree; @@ -24,6 +24,7 @@ describe('ServerCheckGuard', () => { findRoot: jasmine.createSpy('findRoot') }); redirectUrlTree = new UrlTree(); + eventSubject = new ReplaySubject(1); router = { events: eventSubject.asObservable(), navigateByUrl: jasmine.createSpy('navigateByUrl'), @@ -64,10 +65,10 @@ describe('ServerCheckGuard', () => { }); describe(`listenForRouteChanges`, () => { - it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => { + it(`should invalidate the root cache, when the method is first called`, () => { testScheduler.run(() => { guard.listenForRouteChanges(); - expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false); + expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(1); }); }); @@ -80,7 +81,8 @@ describe('ServerCheckGuard', () => { eventSubject.next(new NavigationEnd(2,'', '')); eventSubject.next(new NavigationStart(3,'')); }); - expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3); + // once when the method is first called, and then 3 times for NavigationStart events + expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(1 + 3); }); }); }); diff --git a/src/app/core/server-check/server-check.guard.ts b/src/app/core/server-check/server-check.guard.ts index 65ca2b0c498..79c34c36590 100644 --- a/src/app/core/server-check/server-check.guard.ts +++ b/src/app/core/server-check/server-check.guard.ts @@ -53,10 +53,8 @@ export class ServerCheckGuard implements CanActivateChild { */ listenForRouteChanges(): void { // we'll always be too late for the first NavigationStart event with the router subscribe below, - // so this statement is for the very first route operation. A `find` without using the cache, - // rather than an invalidateRootCache, because invalidating as the app is bootstrapping can - // break other features - this.rootDataService.findRoot(false); + // so this statement is for the very first route operation. + this.rootDataService.invalidateRootCache(); this.router.events.pipe( filter(event => event instanceof NavigationStart), diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 8b6316a6ce2..98ab6a16ea0 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith, switchMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith, switchMap, take, skipWhile } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { EndpointMapRequest } from '../data/request.models'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; @@ -9,7 +9,7 @@ import { EndpointMap } from '../cache/response.models'; import { getFirstCompletedRemoteData } from './operators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; -import { UnCacheableObject } from './uncacheable-object.model'; +import { CacheableObject } from '../cache/cacheable-object.model'; @Injectable() export class HALEndpointService { @@ -33,9 +33,13 @@ export class HALEndpointService { this.requestService.send(request, true); - return this.rdbService.buildFromHref(href).pipe( + return this.rdbService.buildFromHref(href).pipe( + // This skip ensures that if a stale object is present in the cache when you do a + // call it isn't immediately returned, but we wait until the remote data for the new request + // is created. + skipWhile((rd: RemoteData) => rd.isStale), getFirstCompletedRemoteData(), - map((response: RemoteData) => { + map((response: RemoteData) => { if (hasValue(response.payload)) { return response.payload._links; } else { From ca56af6ff5cd1c34e70089574998605ac0242a32 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 1 Dec 2023 16:19:12 +0100 Subject: [PATCH 09/31] also skip loading hal requests --- src/app/core/shared/hal-endpoint.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 98ab6a16ea0..5cdd7ccfad7 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -6,7 +6,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Injectable } from '@angular/core'; import { EndpointMap } from '../cache/response.models'; -import { getFirstCompletedRemoteData } from './operators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; import { CacheableObject } from '../cache/cacheable-object.model'; @@ -37,8 +36,8 @@ export class HALEndpointService { // This skip ensures that if a stale object is present in the cache when you do a // call it isn't immediately returned, but we wait until the remote data for the new request // is created. - skipWhile((rd: RemoteData) => rd.isStale), - getFirstCompletedRemoteData(), + skipWhile((rd: RemoteData) => rd.isLoading || rd.isStale), + take(1), map((response: RemoteData) => { if (hasValue(response.payload)) { return response.payload._links; From 18190c8837ba78c83e920cff9c27b8957fea7dc0 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 6 Dec 2023 11:14:41 +0100 Subject: [PATCH 10/31] add ResponsePendingStale state --- .../core/data/base/base-data.service.spec.ts | 98 ++++---- src/app/core/data/base/base-data.service.ts | 4 +- .../data/request-entry-state.model.spec.ts | 186 +++++++++++++++ .../core/data/request-entry-state.model.ts | 25 +- src/app/core/data/request.reducer.spec.ts | 222 +++++++++++++++--- src/app/core/data/request.reducer.ts | 55 +++-- src/app/core/data/request.service.ts | 2 +- .../core/shared/hal-endpoint.service.spec.ts | 180 +++++++++++--- src/app/core/shared/hal-endpoint.service.ts | 25 +- 9 files changed, 658 insertions(+), 139 deletions(-) create mode 100644 src/app/core/data/request-entry-state.model.spec.ts diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 098f075c101..75662a691fa 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -95,6 +95,7 @@ describe('BaseDataService', () => { remoteDataMocks = { RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), @@ -303,19 +304,21 @@ describe('BaseDataService', () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); @@ -354,19 +357,21 @@ describe('BaseDataService', () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); @@ -487,19 +492,21 @@ describe('BaseDataService', () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); @@ -538,21 +545,24 @@ describe('BaseDataService', () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; + expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); }); }); diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index edd6d9e2a42..c7cd5b0a705 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -273,7 +273,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); @@ -307,7 +307,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); diff --git a/src/app/core/data/request-entry-state.model.spec.ts b/src/app/core/data/request-entry-state.model.spec.ts new file mode 100644 index 00000000000..7daa655566a --- /dev/null +++ b/src/app/core/data/request-entry-state.model.spec.ts @@ -0,0 +1,186 @@ +import { + isRequestPending, + isError, + isSuccess, + isErrorStale, + isSuccessStale, + isResponsePending, + isResponsePendingStale, + isLoading, + isStale, + hasFailed, + hasSucceeded, + hasCompleted, + RequestEntryState +} from './request-entry-state.model'; + +describe(`isRequestPending`, () => { + it(`should only return true if the given state is RequestPending`, () => { + expect(isRequestPending(RequestEntryState.RequestPending)).toBeTrue(); + + expect(isRequestPending(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isRequestPending(RequestEntryState.Error)).toBeFalse(); + expect(isRequestPending(RequestEntryState.Success)).toBeFalse(); + expect(isRequestPending(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isRequestPending(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isRequestPending(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isError`, () => { + it(`should only return true if the given state is Error`, () => { + expect(isError(RequestEntryState.Error)).toBeTrue(); + + expect(isError(RequestEntryState.RequestPending)).toBeFalse(); + expect(isError(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isError(RequestEntryState.Success)).toBeFalse(); + expect(isError(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isError(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isError(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isSuccess`, () => { + it(`should only return true if the given state is Success`, () => { + expect(isSuccess(RequestEntryState.Success)).toBeTrue(); + + expect(isSuccess(RequestEntryState.RequestPending)).toBeFalse(); + expect(isSuccess(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isSuccess(RequestEntryState.Error)).toBeFalse(); + expect(isSuccess(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isSuccess(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isSuccess(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isErrorStale`, () => { + it(`should only return true if the given state is ErrorStale`, () => { + expect(isErrorStale(RequestEntryState.ErrorStale)).toBeTrue(); + + expect(isErrorStale(RequestEntryState.RequestPending)).toBeFalse(); + expect(isErrorStale(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isErrorStale(RequestEntryState.Error)).toBeFalse(); + expect(isErrorStale(RequestEntryState.Success)).toBeFalse(); + expect(isErrorStale(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isErrorStale(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isSuccessStale`, () => { + it(`should only return true if the given state is SuccessStale`, () => { + expect(isSuccessStale(RequestEntryState.SuccessStale)).toBeTrue(); + + expect(isSuccessStale(RequestEntryState.RequestPending)).toBeFalse(); + expect(isSuccessStale(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isSuccessStale(RequestEntryState.Error)).toBeFalse(); + expect(isSuccessStale(RequestEntryState.Success)).toBeFalse(); + expect(isSuccessStale(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isSuccessStale(RequestEntryState.ErrorStale)).toBeFalse(); + }); +}); + +describe(`isResponsePending`, () => { + it(`should only return true if the given state is ResponsePending`, () => { + expect(isResponsePending(RequestEntryState.ResponsePending)).toBeTrue(); + + expect(isResponsePending(RequestEntryState.RequestPending)).toBeFalse(); + expect(isResponsePending(RequestEntryState.Error)).toBeFalse(); + expect(isResponsePending(RequestEntryState.Success)).toBeFalse(); + expect(isResponsePending(RequestEntryState.ResponsePendingStale)).toBeFalse(); + expect(isResponsePending(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isResponsePending(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isResponsePendingStale`, () => { + it(`should only return true if the given state is requestPending`, () => { + expect(isResponsePendingStale(RequestEntryState.ResponsePendingStale)).toBeTrue(); + + expect(isResponsePendingStale(RequestEntryState.RequestPending)).toBeFalse(); + expect(isResponsePendingStale(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isResponsePendingStale(RequestEntryState.Error)).toBeFalse(); + expect(isResponsePendingStale(RequestEntryState.Success)).toBeFalse(); + expect(isResponsePendingStale(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isResponsePendingStale(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`isLoading`, () => { + it(`should only return true if the given state is RequestPending, ResponsePending or ResponsePendingStale`, () => { + expect(isLoading(RequestEntryState.RequestPending)).toBeTrue(); + expect(isLoading(RequestEntryState.ResponsePending)).toBeTrue(); + expect(isLoading(RequestEntryState.ResponsePendingStale)).toBeTrue(); + + expect(isLoading(RequestEntryState.Error)).toBeFalse(); + expect(isLoading(RequestEntryState.Success)).toBeFalse(); + expect(isLoading(RequestEntryState.ErrorStale)).toBeFalse(); + expect(isLoading(RequestEntryState.SuccessStale)).toBeFalse(); + }); +}); + +describe(`hasFailed`, () => { + describe(`when the state is loading`, () => { + it(`should return undefined`, () => { + expect(hasFailed(RequestEntryState.RequestPending)).toBeUndefined(); + expect(hasFailed(RequestEntryState.ResponsePending)).toBeUndefined(); + expect(hasFailed(RequestEntryState.ResponsePendingStale)).toBeUndefined(); + }); + }); + + describe(`when the state has completed`, () => { + it(`should only return true if the given state is Error or ErrorStale`, () => { + expect(hasFailed(RequestEntryState.Error)).toBeTrue(); + expect(hasFailed(RequestEntryState.ErrorStale)).toBeTrue(); + + expect(hasFailed(RequestEntryState.Success)).toBeFalse(); + expect(hasFailed(RequestEntryState.SuccessStale)).toBeFalse(); + }); + }); +}); + +describe(`hasSucceeded`, () => { + describe(`when the state is loading`, () => { + it(`should return undefined`, () => { + expect(hasSucceeded(RequestEntryState.RequestPending)).toBeUndefined(); + expect(hasSucceeded(RequestEntryState.ResponsePending)).toBeUndefined(); + expect(hasSucceeded(RequestEntryState.ResponsePendingStale)).toBeUndefined(); + }); + }); + + describe(`when the state has completed`, () => { + it(`should only return true if the given state is Error or ErrorStale`, () => { + expect(hasSucceeded(RequestEntryState.Success)).toBeTrue(); + expect(hasSucceeded(RequestEntryState.SuccessStale)).toBeTrue(); + + expect(hasSucceeded(RequestEntryState.Error)).toBeFalse(); + expect(hasSucceeded(RequestEntryState.ErrorStale)).toBeFalse(); + }); + }); +}); + + +describe(`hasCompleted`, () => { + it(`should only return true if the given state is Error, Success, ErrorStale or SuccessStale`, () => { + expect(hasCompleted(RequestEntryState.Error)).toBeTrue(); + expect(hasCompleted(RequestEntryState.Success)).toBeTrue(); + expect(hasCompleted(RequestEntryState.ErrorStale)).toBeTrue(); + expect(hasCompleted(RequestEntryState.SuccessStale)).toBeTrue(); + + expect(hasCompleted(RequestEntryState.RequestPending)).toBeFalse(); + expect(hasCompleted(RequestEntryState.ResponsePending)).toBeFalse(); + expect(hasCompleted(RequestEntryState.ResponsePendingStale)).toBeFalse(); + }); +}); + +describe(`isStale`, () => { + it(`should only return true if the given state is ResponsePendingStale, SuccessStale or ErrorStale`, () => { + expect(isStale(RequestEntryState.ResponsePendingStale)).toBeTrue(); + expect(isStale(RequestEntryState.SuccessStale)).toBeTrue(); + expect(isStale(RequestEntryState.ErrorStale)).toBeTrue(); + + expect(isStale(RequestEntryState.RequestPending)).toBeFalse(); + expect(isStale(RequestEntryState.ResponsePending)).toBeFalse(); + expect(isStale(RequestEntryState.Error)).toBeFalse(); + expect(isStale(RequestEntryState.Success)).toBeFalse(); + }); +}); diff --git a/src/app/core/data/request-entry-state.model.ts b/src/app/core/data/request-entry-state.model.ts index a813b6e7436..3aeace39d29 100644 --- a/src/app/core/data/request-entry-state.model.ts +++ b/src/app/core/data/request-entry-state.model.ts @@ -3,8 +3,9 @@ export enum RequestEntryState { ResponsePending = 'ResponsePending', Error = 'Error', Success = 'Success', + ResponsePendingStale = 'ResponsePendingStale', ErrorStale = 'ErrorStale', - SuccessStale = 'SuccessStale' + SuccessStale = 'SuccessStale', } /** @@ -42,12 +43,21 @@ export const isSuccessStale = (state: RequestEntryState) => */ export const isResponsePending = (state: RequestEntryState) => state === RequestEntryState.ResponsePending; + /** - * Returns true if the given state is RequestPending or ResponsePending, - * false otherwise + * Returns true if the given state is ResponsePendingStale, false otherwise + */ +export const isResponsePendingStale = (state: RequestEntryState) => + state === RequestEntryState.ResponsePendingStale; + +/** + * Returns true if the given state is RequestPending, RequestPendingStale, ResponsePending, or + * ResponsePendingStale, false otherwise */ export const isLoading = (state: RequestEntryState) => - isRequestPending(state) || isResponsePending(state); + isRequestPending(state) || + isResponsePending(state) || + isResponsePendingStale(state); /** * If isLoading is true for the given state, this method returns undefined, we can't know yet. @@ -82,7 +92,10 @@ export const hasCompleted = (state: RequestEntryState) => !isLoading(state); /** - * Returns true if the given state is SuccessStale or ErrorStale, false otherwise + * Returns true if the given state is isRequestPendingStale, isResponsePendingStale, SuccessStale or + * ErrorStale, false otherwise */ export const isStale = (state: RequestEntryState) => - isSuccessStale(state) || isErrorStale(state); + isResponsePendingStale(state) || + isSuccessStale(state) || + isErrorStale(state); diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts index 6f2258a2539..30269bd6063 100644 --- a/src/app/core/data/request.reducer.spec.ts +++ b/src/app/core/data/request.reducer.spec.ts @@ -48,9 +48,16 @@ describe('requestReducer', () => { lastUpdated: 0 } }; + const testResponsePendingState = { + [id1]: { + state: RequestEntryState.ResponsePending, + lastUpdated: 0 + } + }; deepFreeze(testInitState); deepFreeze(testSuccessState); deepFreeze(testErrorState); + deepFreeze(testResponsePendingState); it('should return the current state when no valid actions have been made', () => { const action = new NullAction(); @@ -91,29 +98,94 @@ describe('requestReducer', () => { expect(newState[id1].response).toEqual(undefined); }); - it('should set state to Success for the given RestRequest in the state, in response to a SUCCESS action', () => { - const state = testInitState; + describe(`in response to a SUCCESS action`, () => { + let startState; + describe(`when the entry isn't stale`, () => { + beforeEach(() => { + startState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ResponsePending + }) + }); + deepFreeze(startState); + }); + it('should set state to Success for the given RestRequest in the state', () => { + const action = new RequestSuccessAction(id1, 200); + const newState = requestReducer(startState, action); + + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].state).toEqual(RequestEntryState.Success); + expect(newState[id1].response.statusCode).toEqual(200); + }); + }); - const action = new RequestSuccessAction(id1, 200); - const newState = requestReducer(state, action); + describe(`when the entry is stale`, () => { + beforeEach(() => { + startState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ResponsePendingStale + }) + }); + deepFreeze(startState); + }); + it('should set state to SuccessStale for the given RestRequest in the state', () => { + const action = new RequestSuccessAction(id1, 200); + const newState = requestReducer(startState, action); + + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].state).toEqual(RequestEntryState.SuccessStale); + expect(newState[id1].response.statusCode).toEqual(200); + }); + }); - expect(newState[id1].request.uuid).toEqual(id1); - expect(newState[id1].request.href).toEqual(link1); - expect(newState[id1].state).toEqual(RequestEntryState.Success); - expect(newState[id1].response.statusCode).toEqual(200); }); - it('should set state to Error for the given RestRequest in the state, in response to an ERROR action', () => { - const state = testInitState; + describe(`in response to an ERROR action`, () => { + let startState; + describe(`when the entry isn't stale`, () => { + beforeEach(() => { + startState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ResponsePending + }) + }); + deepFreeze(startState); + }); + it('should set state to Error for the given RestRequest in the state', () => { + const action = new RequestErrorAction(id1, 404, 'Not Found'); + const newState = requestReducer(startState, action); + + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].state).toEqual(RequestEntryState.Error); + expect(newState[id1].response.statusCode).toEqual(404); + expect(newState[id1].response.errorMessage).toEqual('Not Found'); + }); + }); - const action = new RequestErrorAction(id1, 404, 'Not Found'); - const newState = requestReducer(state, action); + describe(`when the entry is stale`, () => { + beforeEach(() => { + startState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ResponsePendingStale + }) + }); + deepFreeze(startState); + }); + it('should set state to ErrorStale for the given RestRequest in the state', () => { + const action = new RequestErrorAction(id1, 404, 'Not Found'); + const newState = requestReducer(startState, action); + + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].state).toEqual(RequestEntryState.ErrorStale); + expect(newState[id1].response.statusCode).toEqual(404); + expect(newState[id1].response.errorMessage).toEqual('Not Found'); + }); - expect(newState[id1].request.uuid).toEqual(id1); - expect(newState[id1].request.href).toEqual(link1); - expect(newState[id1].state).toEqual(RequestEntryState.Error); - expect(newState[id1].response.statusCode).toEqual(404); - expect(newState[id1].response.errorMessage).toEqual('Not Found'); + }); }); it('should set state to Error for the given RestRequest in the state, in response to an ERROR action with pathable errors', () => { @@ -175,28 +247,112 @@ describe('requestReducer', () => { expect(newState[id1]).toBeNull(); }); - describe(`for an entry with state: Success`, () => { - it(`should set the state to SuccessStale, in response to a STALE action`, () => { - const state = testSuccessState; + describe(`in response to a STALE action`, () => { + describe(`when the entry has been removed`, () => { + it(`shouldn't do anything`, () => { + const startState = { + [id1]: null + }; + deepFreeze(startState); - const action = new RequestStaleAction(id1); - const newState = requestReducer(state, action); + const action = new RequestStaleAction(id1); + const newState = requestReducer(startState, action); - expect(newState[id1].state).toEqual(RequestEntryState.SuccessStale); - expect(newState[id1].lastUpdated).toBe(action.lastUpdated); + expect(newState[id1]).toBeNull(); + }); }); - }); - describe(`for an entry with state: Error`, () => { - it(`should set the state to ErrorStale, in response to a STALE action`, () => { - const state = testErrorState; + describe(`for stale entries`, () => { + it(`shouldn't do anything`, () => { + const rpsStartState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ResponsePendingStale + }) + }); + deepFreeze(rpsStartState); + + const action = new RequestStaleAction(id1); + let newState = requestReducer(rpsStartState, action); + + expect(newState[id1].state).toEqual(rpsStartState[id1].state); + expect(newState[id1].lastUpdated).toBe(rpsStartState[id1].lastUpdated); + + const ssStartState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.SuccessStale + }) + }); - const action = new RequestStaleAction(id1); - const newState = requestReducer(state, action); + newState = requestReducer(ssStartState, action); - expect(newState[id1].state).toEqual(RequestEntryState.ErrorStale); - expect(newState[id1].lastUpdated).toBe(action.lastUpdated); + expect(newState[id1].state).toEqual(ssStartState[id1].state); + expect(newState[id1].lastUpdated).toBe(ssStartState[id1].lastUpdated); + + const esStartState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.ErrorStale + }) + }); + + newState = requestReducer(esStartState, action); + + expect(newState[id1].state).toEqual(esStartState[id1].state); + expect(newState[id1].lastUpdated).toBe(esStartState[id1].lastUpdated); + + }); }); - }); + describe(`for and entry with state: RequestPending`, () => { + it(`shouldn't do anything`, () => { + const startState = Object.assign({}, testInitState, { + [id1]: Object.assign({}, testInitState[id1], { + state: RequestEntryState.RequestPending + }) + }); + + const action = new RequestStaleAction(id1); + const newState = requestReducer(startState, action); + + expect(newState[id1].state).toEqual(startState[id1].state); + expect(newState[id1].lastUpdated).toBe(startState[id1].lastUpdated); + + }); + }); + + describe(`for an entry with state: ResponsePending`, () => { + it(`should set the state to ResponsePendingStale`, () => { + const state = testResponsePendingState; + + const action = new RequestStaleAction(id1); + const newState = requestReducer(state, action); + + expect(newState[id1].state).toEqual(RequestEntryState.ResponsePendingStale); + expect(newState[id1].lastUpdated).toBe(action.lastUpdated); + }); + }); + + describe(`for an entry with state: Success`, () => { + it(`should set the state to SuccessStale`, () => { + const state = testSuccessState; + + const action = new RequestStaleAction(id1); + const newState = requestReducer(state, action); + + expect(newState[id1].state).toEqual(RequestEntryState.SuccessStale); + expect(newState[id1].lastUpdated).toBe(action.lastUpdated); + }); + }); + + describe(`for an entry with state: Error`, () => { + it(`should set the state to ErrorStale`, () => { + const state = testErrorState; + + const action = new RequestStaleAction(id1); + const newState = requestReducer(state, action); + + expect(newState[id1].state).toEqual(RequestEntryState.ErrorStale); + expect(newState[id1].lastUpdated).toBe(action.lastUpdated); + }); + }); + }); }); diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index 94f1f638c79..858e0a13714 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -11,7 +11,13 @@ import { ResetResponseTimestampsAction } from './request.actions'; import { isNull } from '../../shared/empty.util'; -import { hasSucceeded, isStale, RequestEntryState } from './request-entry-state.model'; +import { + hasSucceeded, + isStale, + RequestEntryState, + isRequestPending, + isResponsePending +} from './request-entry-state.model'; import { RequestState } from './request-state.model'; // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) @@ -91,14 +97,17 @@ function executeRequest(storeState: RequestState, action: RequestExecuteAction): * the new storeState, with the response added to the request */ function completeSuccessRequest(storeState: RequestState, action: RequestSuccessAction): RequestState { - if (isNull(storeState[action.payload.uuid])) { + const prevEntry = storeState[action.payload.uuid]; + if (isNull(prevEntry)) { // after a request has been removed it's possible pending changes still come in. // Don't store them return storeState; } else { return Object.assign({}, storeState, { - [action.payload.uuid]: Object.assign({}, storeState[action.payload.uuid], { - state: RequestEntryState.Success, + [action.payload.uuid]: Object.assign({}, prevEntry, { + // If a response comes in for a request that's already stale, still store it otherwise + // components that are waiting for it might freeze + state: isStale(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.Success, response: { timeCompleted: action.payload.timeCompleted, lastUpdated: action.payload.timeCompleted, @@ -124,14 +133,17 @@ function completeSuccessRequest(storeState: RequestState, action: RequestSuccess * the new storeState, with the response added to the request */ function completeFailedRequest(storeState: RequestState, action: RequestErrorAction): RequestState { - if (isNull(storeState[action.payload.uuid])) { + const prevEntry = storeState[action.payload.uuid]; + if (isNull(prevEntry)) { // after a request has been removed it's possible pending changes still come in. // Don't store them return storeState; } else { return Object.assign({}, storeState, { - [action.payload.uuid]: Object.assign({}, storeState[action.payload.uuid], { - state: RequestEntryState.Error, + [action.payload.uuid]: Object.assign({}, prevEntry, { + // If a response comes in for a request that's already stale, still store it otherwise + // components that are waiting for it might freeze + state: isStale(prevEntry.state) ? RequestEntryState.ErrorStale : RequestEntryState.Error, response: { timeCompleted: action.payload.timeCompleted, lastUpdated: action.payload.timeCompleted, @@ -156,22 +168,27 @@ function completeFailedRequest(storeState: RequestState, action: RequestErrorAct * the new storeState, set to stale */ function expireRequest(storeState: RequestState, action: RequestStaleAction): RequestState { - if (isNull(storeState[action.payload.uuid])) { - // after a request has been removed it's possible pending changes still come in. - // Don't store them + const prevEntry = storeState[action.payload.uuid]; + if (isNull(prevEntry) || isStale(prevEntry.state) || isRequestPending(prevEntry.state)) { + // No need to do anything if the entry doesn't exist, is already stale, or if the request is + // still pending, because that means it still needs to be sent to the server. Any response + // is guaranteed to have been generated after the request was set to stale. return storeState; } else { - const prevEntry = storeState[action.payload.uuid]; - if (isStale(prevEntry.state)) { - return storeState; + let nextRequestEntryState: RequestEntryState; + if (isResponsePending(prevEntry.state)) { + nextRequestEntryState = RequestEntryState.ResponsePendingStale; + } else if (hasSucceeded(prevEntry.state)) { + nextRequestEntryState = RequestEntryState.SuccessStale; } else { - return Object.assign({}, storeState, { - [action.payload.uuid]: Object.assign({}, prevEntry, { - state: hasSucceeded(prevEntry.state) ? RequestEntryState.SuccessStale : RequestEntryState.ErrorStale, - lastUpdated: action.lastUpdated - }) - }); + nextRequestEntryState = RequestEntryState.ErrorStale; } + return Object.assign({}, storeState, { + [action.payload.uuid]: Object.assign({}, prevEntry, { + state: nextRequestEntryState, + lastUpdated: action.lastUpdated + }) + }); } } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index ec633370ce2..9f43c3f5992 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -164,7 +164,7 @@ export class RequestService { this.getByHref(request.href).pipe( take(1)) .subscribe((re: RequestEntry) => { - isPending = (hasValue(re) && isLoading(re.state)); + isPending = (hasValue(re) && isLoading(re.state) && !isStale(re.state)); }); return isPending; } diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 56e890b3189..b81d0806dfd 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -1,4 +1,3 @@ -import { cold, hot } from 'jasmine-marbles'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from './hal-endpoint.service'; @@ -7,12 +6,17 @@ import { combineLatest as observableCombineLatest, of as observableOf } from 'rx import { environment } from '../../../environments/environment'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { TestScheduler } from 'rxjs/testing'; +import { RemoteData } from '../data/remote-data'; +import { RequestEntryState } from '../data/request-entry-state.model'; describe('HALEndpointService', () => { let service: HALEndpointService; let requestService: RequestService; let rdbService: RemoteDataBuildService; let envConfig; + let testScheduler; + let remoteDataMocks; const endpointMap = { test: { href: 'https://rest.api/test' @@ -68,7 +72,30 @@ describe('HALEndpointService', () => { }; const linkPath = 'test'; + const timeStamp = new Date().getTime(); + const msToLive = 15 * 60 * 1000; + const payload = { + _links: endpointMaps[one] + }; + const statusCodeSuccess = 200; + const statusCodeError = 404; + const errorMessage = 'not found'; + remoteDataMocks = { + RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), + Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), + SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), + Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + }; + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + // asserting the two objects are equal + // e.g. using chai. + expect(actual).toEqual(expected); + }); requestService = getMockRequestService(); rdbService = jasmine.createSpyObj('rdbService', { buildFromHref: createSuccessfulRemoteDataObject$({ @@ -111,20 +138,28 @@ describe('HALEndpointService', () => { }); it(`should return the endpoint URL for the service's linkPath`, () => { - spyOn(service as any, 'getEndpointAt').and - .returnValue(hot('a-', { a: 'https://rest.api/test' })); - const result = service.getEndpoint(linkPath); - - const expected = cold('(b|)', { b: endpointMap.test.href }); - expect(result).toBeObservable(expected); + testScheduler.run(({ cold, expectObservable }) => { + spyOn(service as any, 'getEndpointAt').and + .returnValue(cold('a-', { a: 'https://rest.api/test' })); + const result = service.getEndpoint(linkPath); + + const expected = '(b|)'; + const values = { + b: endpointMap.test.href + }; + expectObservable(result).toBe(expected, values); + }); }); it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { - spyOn(service as any, 'getEndpointAt').and - .returnValue(hot('a-', { a: undefined })); - const result = service.getEndpoint('unknown'); - const expected = cold('(b|)', { b: undefined }); - expect(result).toBeObservable(expected); + testScheduler.run(({ cold, expectObservable }) => { + spyOn(service as any, 'getEndpointAt').and + .returnValue(cold('a-', { a: undefined })); + const result = service.getEndpoint('unknown'); + const expected = '(b|)'; + const values = { b: undefined }; + expectObservable(result).toBe(expected, values); + }); }); }); @@ -183,29 +218,118 @@ describe('HALEndpointService', () => { }); it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => { - spyOn(service as any, 'getRootEndpointMap').and - .returnValue(hot('----')); - - const result = service.isEnabledOnRestApi(linkPath); - const expected = cold('b---', { b: undefined }); - expect(result).toBeObservable(expected); + testScheduler.run(({ cold, expectObservable }) => { + spyOn(service as any, 'getRootEndpointMap').and + .returnValue(cold('----')); + + const result = service.isEnabledOnRestApi(linkPath); + const expected = 'b---'; + const values = { b: undefined }; + expectObservable(result).toBe(expected, values); + }); }); it('should return true if the service\'s linkPath is in the endpoint map', () => { - spyOn(service as any, 'getRootEndpointMap').and - .returnValue(hot('--a-', { a: endpointMap })); - const result = service.isEnabledOnRestApi(linkPath); - const expected = cold('b-c-', { b: undefined, c: true }); - expect(result).toBeObservable(expected); + testScheduler.run(({ cold, expectObservable }) => { + spyOn(service as any, 'getRootEndpointMap').and + .returnValue(cold('--a-', { a: endpointMap })); + const result = service.isEnabledOnRestApi(linkPath); + const expected = 'b-c-'; + const values = { b: undefined, c: true }; + expectObservable(result).toBe(expected, values); + }); }); it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => { - spyOn(service as any, 'getRootEndpointMap').and - .returnValue(hot('--a-', { a: endpointMap })); + testScheduler.run(({ cold, expectObservable }) => { + spyOn(service as any, 'getRootEndpointMap').and + .returnValue(cold('--a-', { a: endpointMap })); + + const result = service.isEnabledOnRestApi('unknown'); + const expected = 'b-c-'; + const values = { b: undefined, c: false }; + expectObservable(result).toBe(expected, values); + }); + }); + + }); + + describe(`getEndpointMapAt`, () => { + const href = 'https://rest.api/some/sub/path'; + + it(`should call requestService.send with a new EndpointMapRequest for the given href. useCachedVersionIfAvailable should be true`, () => { + testScheduler.run(() => { + (service as any).getEndpointMapAt(href); + }); + const expected = new EndpointMapRequest(requestService.generateRequestId(), href); + expect(requestService.send).toHaveBeenCalledWith(expected, true); + }); + + it(`should call rdbService.buildFromHref with the given href`, () => { + testScheduler.run(() => { + (service as any).getEndpointMapAt(href); + }); + expect(rdbService.buildFromHref).toHaveBeenCalledWith(href); + }); + + describe(`when the RemoteData returned from rdbService is stale`, () => { + it(`should re-request it`, () => { + spyOn(service as any, 'getEndpointMapAt').and.callThrough(); + testScheduler.run(({ cold }) => { + (rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', { a: remoteDataMocks.ResponsePendingStale })); + // we need to subscribe to the result, to ensure the "tap" that does the re-request can fire + (service as any).getEndpointMapAt(href).subscribe(); + }); + expect((service as any).getEndpointMapAt).toHaveBeenCalledTimes(2); + }); + }); + + describe(`when the RemoteData returned from rdbService isn't stale`, () => { + it(`should not re-request it`, () => { + spyOn(service as any, 'getEndpointMapAt').and.callThrough(); + testScheduler.run(({ cold }) => { + (rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', { a: remoteDataMocks.ResponsePending })); + // we need to subscribe to the result, to ensure the "tap" that does the re-request can fire + (service as any).getEndpointMapAt(href).subscribe(); + }); + expect((service as any).getEndpointMapAt).toHaveBeenCalledTimes(1); + }); + }); - const result = service.isEnabledOnRestApi('unknown'); - const expected = cold('b-c-', { b: undefined, c: false }); - expect(result).toBeObservable(expected); + it(`should emit exactly once, returning the endpoint map in the response, when the RemoteData completes`, () => { + testScheduler.run(({ cold, expectObservable }) => { + (rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a-b-c-d-e-f-g-h-i-j-k-l', { + a: remoteDataMocks.RequestPending, + b: remoteDataMocks.ResponsePending, + c: remoteDataMocks.ResponsePendingStale, + d: remoteDataMocks.SuccessStale, + e: remoteDataMocks.RequestPending, + f: remoteDataMocks.ResponsePending, + g: remoteDataMocks.Success, + h: remoteDataMocks.SuccessStale, + i: remoteDataMocks.RequestPending, + k: remoteDataMocks.ResponsePending, + l: remoteDataMocks.Error, + })); + const expected = '------------(g|)'; + const values = { + g: endpointMaps[one] + }; + expectObservable((service as any).getEndpointMapAt(one)).toBe(expected, values); + }); + }); + + it(`should emit undefined when the response doesn't have a payload`, () => { + testScheduler.run(({ cold, expectObservable }) => { + (rdbService.buildFromHref as jasmine.Spy).and.returnValue(cold('a', { + a: remoteDataMocks.Error, + })); + const expected = '(a|)'; + const values = { + g: undefined + }; + expectObservable((service as any).getEndpointMapAt(href)).toBe(expected, values); + }); }); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 5cdd7ccfad7..07754616c73 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,11 +1,19 @@ import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith, switchMap, take, skipWhile } from 'rxjs/operators'; +import { + distinctUntilChanged, + map, + startWith, + switchMap, + take, + tap, filter +} from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { EndpointMapRequest } from '../data/request.models'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Injectable } from '@angular/core'; import { EndpointMap } from '../cache/response.models'; +import { getFirstCompletedRemoteData } from './operators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; import { CacheableObject } from '../cache/cacheable-object.model'; @@ -33,11 +41,16 @@ export class HALEndpointService { this.requestService.send(request, true); return this.rdbService.buildFromHref(href).pipe( - // This skip ensures that if a stale object is present in the cache when you do a - // call it isn't immediately returned, but we wait until the remote data for the new request - // is created. - skipWhile((rd: RemoteData) => rd.isLoading || rd.isStale), - take(1), + // Re-request stale responses + tap((rd: RemoteData) => { + if (hasValue(rd) && rd.isStale) { + this.getEndpointMapAt(href); + } + }), + // Filter out all stale responses. We're only interested in a single, non-stale, + // completed RemoteData + filter((rd: RemoteData) => !rd.isStale), + getFirstCompletedRemoteData(), map((response: RemoteData) => { if (hasValue(response.payload)) { return response.payload._links; From 461866006f97b8b04b5041cafc977c294c06918f Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Thu, 4 Jan 2024 11:44:49 +0100 Subject: [PATCH 11/31] [DSC-1066] search-filter query with [ ] --- src/app/core/shared/search/search-configuration.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 7708523dea6..66c82cf1a31 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -164,7 +164,8 @@ export class SearchConfigurationService implements OnDestroy { */ getCurrentQuery(defaultQuery: string) { return this.routeService.getQueryParameterValue('query').pipe(map((query) => { - return query || defaultQuery; + const queryFromURL = query || defaultQuery; + return decodeURIComponent(queryFromURL) ?? ''; })); } From f9ea92939e938cbee5529890a5c47d10a1d81d44 Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Thu, 4 Jan 2024 16:01:30 +0100 Subject: [PATCH 12/31] [DSC-1073] specify the communityorCollection discovery configuration on item-collection mapper --- .../item-collection-mapper/item-collection-mapper.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index d94abfaa9f8..c466b0d0f2c 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 @@ -148,7 +148,8 @@ export class ItemCollectionMapperComponent implements OnInit { switchMap(([itemCollectionsRD, owningCollectionRD, searchOptions]) => { return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), { query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query), - dsoTypes: [DSpaceObjectType.COLLECTION] + dsoTypes: [DSpaceObjectType.COLLECTION], + configuration: 'communityorCollection', }), 10000).pipe( toDSpaceObjectListRD(), startWith(undefined) From 8943e8579a5e5bc50ff18af792bb9185cd40a72a Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Sat, 6 Jan 2024 14:37:12 +0100 Subject: [PATCH 13/31] Target the development branch to the next 2024.01.00 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcdeda6feff..a107e626843 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dspace-angular", - "version": "2023.02.01", + "version": "2024.01.00-SNAPSHOT", "scripts": { "ng": "ng", "config:watch": "nodemon", From d8949ee31082bb5ca1070aeeac504b9e0f5bcabf Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 Jan 2024 17:31:20 +0100 Subject: [PATCH 14/31] [DSC-1477] Rename branch in pipeline settings --- bitbucket-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 5cdc81bfd51..ae1f437bf8b 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -20,7 +20,7 @@ definitions: pipelines: branches: - 'dspace-cris-7': + 'main-cris': - step: *unittest-code-checks pull-requests: '**': From efe6a5bc4fa30f7afc3fdb028b847da86f921d8f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 11 Jan 2024 17:44:41 +0100 Subject: [PATCH 15/31] [DSC-1477] Rename branch in references --- src/app/footer/footer.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 9d623359edb..ee337b75d43 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -12,7 +12,7 @@
Get Involved!
  • - + Source Code
  • From 77aa8c5119a24bbd9ebdaefd044731ef02786c89 Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Fri, 12 Jan 2024 15:25:00 +0100 Subject: [PATCH 16/31] [DSC-1456] feature: filters visible in the search sidebar --- .../search/search-filters/search-filters.component.html | 2 +- .../search-filters/search-filters.component.spec.ts | 2 +- .../search/search-filters/search-filters.component.ts | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index 2909d31766e..d8762ed0331 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -1,4 +1,4 @@ -
    +

    {{"search.filters.head" | translate}}

    diff --git a/src/app/shared/search/search-filters/search-filters.component.spec.ts b/src/app/shared/search/search-filters/search-filters.component.spec.ts index 247df67ee53..c3a876c661b 100644 --- a/src/app/shared/search/search-filters/search-filters.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filters.component.spec.ts @@ -76,7 +76,7 @@ describe('SearchFiltersComponent', () => { it('should not render component', () => { const menu = fixture.debugElement.query(By.css('div.d-none')); expect(menu).not.toBeNull(); - expect(comp.availableFilters).toBeFalse(); + expect(comp.searchFilterCount).toEqual(0); }); }); diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index c20a7e37d0f..bdd9729c4d0 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -63,7 +63,7 @@ export class SearchFiltersComponent implements OnInit, AfterViewChecked, OnDestr /** * counts for the active filters */ - availableFilters = false; + searchFilterCount = 0; /** * Link to the search page @@ -112,9 +112,11 @@ export class SearchFiltersComponent implements OnInit, AfterViewChecked, OnDestr } ngAfterViewChecked() { - this.availableFilters = false; + this.searchFilterCount = 0; this.searchFilter._results.forEach(element => { - this.availableFilters = element.nativeElement?.children[0]?.children.length > 0; + if (element.nativeElement?.children[0]?.children.length > 0) { + this.searchFilterCount++; + } }); } From e3409617278e9574a91f3568575520c87793155d Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Mon, 15 Jan 2024 16:22:21 +0100 Subject: [PATCH 17/31] [DSC-1485] feature: csv and xls metadata export find any community --- .../export-metadata-csv-selector.component.ts | 2 +- .../export-metadata-xls-selector.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts index 630a554141b..2ea439512ee 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts @@ -31,7 +31,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ExportMetadataCsvSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION, DSpaceObjectType.COMMUNITY]; action = SelectorActionType.EXPORT_METADATA_CSV; diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts index 9f1a4dfc9ee..68c9349b6d8 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts @@ -28,7 +28,7 @@ import { getProcessDetailRoute } from '../../../../process-page/process-page-rou templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ExportMetadataXlsSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.EXPORT_METADATA_XLS; From e3ff3f8b03a48554f8e96a00b2749e7a83fd6874 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Fri, 10 Nov 2023 13:28:52 +0100 Subject: [PATCH 18/31] [CST-7675] added default file thumbnail --- .../rendering-types/thumbnail/thumbnail.component.ts | 4 +++- src/assets/images/file-placeholder.svg | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/assets/images/file-placeholder.svg diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts index a0dd0a3fe29..cc034068556 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts @@ -96,11 +96,13 @@ export class ThumbnailComponent extends BitstreamRenderingModelComponent impleme */ setDefaultImage(): void { const eType = this.item.firstMetadataValue('dspace.entity.type'); - this.default = 'assets/images/person-placeholder.svg'; + this.default = 'assets/images/file-placeholder.svg'; if (hasValue(eType) && eType.toUpperCase() === 'PROJECT') { this.default = 'assets/images/project-placeholder.svg'; } else if (hasValue(eType) && eType.toUpperCase() === 'ORGUNIT') { this.default = 'assets/images/orgunit-placeholder.svg'; + } else if (hasValue(eType) && eType.toUpperCase() === 'PERSON') { + this.default = 'assets/images/person-placeholder.svg'; } } } diff --git a/src/assets/images/file-placeholder.svg b/src/assets/images/file-placeholder.svg new file mode 100644 index 00000000000..0568c769381 --- /dev/null +++ b/src/assets/images/file-placeholder.svg @@ -0,0 +1,8 @@ + + file-line + + + + + + From 8c24a3d52c3b13aee73397b6ac1fb4735eaf2e26 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Tue, 16 Jan 2024 11:07:17 +0100 Subject: [PATCH 19/31] [DSC-1383] Fix metrics layout and responsivity --- .../cris-layout-metrics-box.component.html | 6 +- .../cris-layout-metrics-box.component.scss | 9 +- .../cris-layout-metrics-box.component.ts | 21 +-- .../metric-altmetric.component.html | 34 +++-- .../metric-altmetric.component.spec.ts | 2 +- .../metric-dimensions.component.html | 28 ++-- .../metric-dimensions.component.spec.ts | 2 +- .../metric-googlescholar.component.html | 4 +- .../metric-loader/base-metric.component.scss | 126 +++++++++--------- .../metric-loader.component.html | 2 +- .../metric-plumx/metric-plumx.component.html | 5 +- .../metric-style-config.pipe.ts | 3 +- src/assets/i18n/en.json5 | 4 +- src/config/default-app-config.ts | 10 +- 14 files changed, 130 insertions(+), 126 deletions(-) diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.html b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.html index feb1c6c351e..0eac2e20aa8 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.html +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.html @@ -1,10 +1,10 @@ -
    -
    +
    +
    + class="d-flex flex-row flex-wrap align-items-center gap-3">
    diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.scss b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.scss index b2d13ac0650..c846adec5ce 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.scss +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.scss @@ -2,11 +2,12 @@ border: 1px solid black; border-radius: 3px; } -:host { - display: flex; - // flex:1; -} ngb-accordion { flex-grow: 1; } + +// This class should be applied to the altmetrics box in the CRIS layout tool +.altmetrics-wrapper { + padding: 15px 70px; +} diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts index 4356141256d..a6b771a8ad8 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metrics/cris-layout-metrics-box.component.ts @@ -67,15 +67,18 @@ export class CrisLayoutMetricsBoxComponent extends CrisLayoutBoxModelComponent i if (isPlatformBrowser(this.platformId)) { this.metricsBoxConfiguration = this.box.configuration as MetricsBoxConfiguration; this.subs.push( - this.itemService.getMetrics(this.item.uuid).pipe(getFirstSucceededRemoteDataPayload()) - .subscribe((result) => { - const matchingMetrics = this.metricsComponentService.getMatchingMetrics( - result.page, - this.metricsBoxConfiguration.maxColumns, - this.metricsBoxConfiguration.metrics - ); - this.metricRows.next(matchingMetrics); - })); + this.itemService.getMetrics(this.item.uuid).pipe( + getFirstSucceededRemoteDataPayload(), + ).subscribe((result) => { + const matchingMetrics = this.metricsComponentService.getMatchingMetrics( + result.page, + this.metricsBoxConfiguration.maxColumns, + this.metricsBoxConfiguration.metrics, + ); + this.metricRows.next(matchingMetrics); + }, + ), + ); } } diff --git a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.html b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.html index 805dc630900..c5e2465c24d 100644 --- a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.html +++ b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.html @@ -1,23 +1,19 @@ -
    -
    -
    -
    -
    +
    +
    -
    -
    - {{ metric.metricType | translate }} -
    +
    + {{ "item.page.metric.label." + metric.metricType | translate }}
    diff --git a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts index 7a4843efd37..d9c64fc09ef 100644 --- a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts +++ b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts @@ -50,7 +50,7 @@ describe('MetricAltmetricComponent', () => { expect(component).toBeTruthy(); }); it('should render badge div', () => { - const div = fixture.debugElement.queryAll(By.css('div'))[3]; + const div = fixture.debugElement.queryAll(By.css('div'))[2]; expect(div.nativeElement.className).toEqual('altmetric-embed'); expect(div.nativeElement.dataset.badgePopover).toEqual('bottom'); expect(div.nativeElement.dataset.doi).toEqual('10.1056/Test'); diff --git a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.html b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.html index eeb4eed38e8..b06b1ab381d 100644 --- a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.html +++ b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.html @@ -1,23 +1,19 @@ -
    -
    -
    -
    -
    -
    - {{ metric.metricType | translate }} -
    + [attr.data-doi]="remark | dsListMetricProps: 'data-doi':isListElement" + [attr.data-style]="remark | dsListMetricProps: 'data-style':isListElement" + [attr.data-legend]="remark | dsListMetricProps: 'data-legend':isListElement" + >
    +
    + {{ "item.page.metric.label." + metric.metricType | translate }}
    diff --git a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts index 69f6c7bcb2f..5c8397989ea 100644 --- a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts +++ b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts @@ -49,7 +49,7 @@ describe('MetricDimensionsComponent', () => { expect(component).toBeTruthy(); }); it('should render badge div', () => { - const div = fixture.debugElement.queryAll(By.css('div'))[2]; + const div = fixture.debugElement.queryAll(By.css('div'))[1]; expect(div.nativeElement.className).toEqual('__dimensions_badge_embed__'); expect(div.nativeElement.dataset.doi).toEqual('10.1056/Test'); expect(div.nativeElement.dataset.style).toEqual('small_rectangle'); diff --git a/src/app/shared/metric/metric-googlescholar/metric-googlescholar.component.html b/src/app/shared/metric/metric-googlescholar/metric-googlescholar.component.html index 58406f59d71..9df3c431d8c 100644 --- a/src/app/shared/metric/metric-googlescholar/metric-googlescholar.component.html +++ b/src/app/shared/metric/metric-googlescholar/metric-googlescholar.component.html @@ -6,8 +6,8 @@
    -
    - {{ metric.metricType | translate }} +
    + {{ "item.page.metric.label." + metric.metricType | translate }}
    diff --git a/src/app/shared/metric/metric-loader/base-metric.component.scss b/src/app/shared/metric/metric-loader/base-metric.component.scss index 6aadd2a7c71..6c98e33d1bf 100644 --- a/src/app/shared/metric/metric-loader/base-metric.component.scss +++ b/src/app/shared/metric/metric-loader/base-metric.component.scss @@ -1,16 +1,14 @@ ::ng-deep { - .container.plumX.alert.metric-container, - .container.altmetric.alert.metric-container, - .container.dimensions.alert.metric-container { - // .plumX & .altmetric & dimensions-> metricType dimensions - display: contents; - } + .metric-container.alert { - .container.alert.metric-container { max-height: inherit; min-height: 7.1em; height: 100%; + margin: 0; + + width: 250px; + .btn-overlap-container { display: none !important; z-index: 1; @@ -20,81 +18,87 @@ color: inherit; text-decoration: auto; } - } - .container.alert.metric-container:hover { + &:hover { opacity: 0.7; - .btn-overlap-container { - margin-top: 0.5em; - display: inline-flex !important; - z-index: 3; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - -webkit-box-shadow: inset 0 1px 0 rgb(255 255 255 / 15%), + .btn-overlap-container { + margin-top: 0.5em; + display: inline-flex !important; + z-index: 3; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + -webkit-box-shadow: inset 0 1px 0 rgb(255 255 255 / 15%), 0 1px 1px rgb(0 0 0 / 8%); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 15%), + box-shadow: inset 0 1px 0 rgb(255 255 255 / 15%), 0 1px 1px rgb(0 0 0 / 8%); + } } - } - .alert.metric-container.alert-info { - .btn-overlap-container { - background-color: #113d4f; - border-color: #113d4f; + .plumX, .altmetric, .dimensions { + // .plumX & .altmetric & dimensions-> metricType dimensions + display: contents; } - } - .alert.metric-container.alert-success { - .btn-overlap-container { - background-color: #4c6722; - border-color: #4c6722; + &.alert-info { + .btn-overlap-container { + background-color: #113d4f; + border-color: #113d4f; + } } - } - .alert.metric-container.alert-danger { - .btn-overlap-container { - background-color: #6c2323; - border-color: #6c2323; + &.alert-success { + .btn-overlap-container { + background-color: #4c6722; + border-color: #4c6722; + } } - } - .alert.metric-container.alert-primary { - .btn-overlap-container { - background-color: #004085; - border-color: #004085; + &.alert-danger { + .btn-overlap-container { + background-color: #6c2323; + border-color: #6c2323; + } } - } - .alert.metric-container.alert-secondary { - .btn-overlap-container { - background-color: #383d41; - border-color: #383d41; + &.alert-primary { + .btn-overlap-container { + background-color: #004085; + border-color: #004085; + } } - } - .alert.metric-container.alert-warning { - .btn-overlap-container { - background-color: #7b4d1b; - border-color: #7b4d1b; + &.alert-secondary { + .btn-overlap-container { + background-color: #383d41; + border-color: #383d41; + } } - } - .alert.metric-container.alert-dark { - .btn-overlap-container { - background-color: #1b1e21; - border-color: #1b1e21; + &.alert-warning { + .btn-overlap-container { + background-color: #7b4d1b; + border-color: #7b4d1b; + } } - } - .alert.metric-container.alert-light { - .btn-overlap-container { - background-color: #818182; - border-color: #818182; - color: black !important; + &.alert-dark { + .btn-overlap-container { + background-color: #1b1e21; + border-color: #1b1e21; + } } + + &.alert-light { + .btn-overlap-container { + background-color: #818182; + border-color: #818182; + color: black !important; + } + } + } } diff --git a/src/app/shared/metric/metric-loader/metric-loader.component.html b/src/app/shared/metric/metric-loader/metric-loader.component.html index b8ca0e069c2..d8c634bfe65 100644 --- a/src/app/shared/metric/metric-loader/metric-loader.component.html +++ b/src/app/shared/metric/metric-loader/metric-loader.component.html @@ -1,3 +1,3 @@ -
  • +
  • {{ 'form.entry.source' | translate}} : {{entry.source}}
  • {{entry.value}}
  • +
  • {{ 'form.entry.source' | translate }} : {{entry.source}}
diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 16d280a94db..4b103dd060b 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -248,6 +248,7 @@ export class FormBuilderService extends DynamicFormService { (controlModel as any).metadataValue.place, (controlModel as any).metadataValue.confidence, (controlModel as any).metadataValue.otherInformation, + (controlModel as any).metadataValue.source, (controlModel as any).metadataValue.metadata); } @@ -262,6 +263,7 @@ export class FormBuilderService extends DynamicFormService { (controlModel as any).value.place, (controlModel as any).value.confidence, (controlModel as any).value.otherInformation, + (controlModel as any).value.source, (controlModel as any).value.metadata); } } diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts index 3d727f05e9d..778c4466ba7 100644 --- a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -25,6 +25,7 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { place: number; label: string; securityLevel: number; + source: string; otherInformation: OtherInformation; constructor(value: any = null, @@ -35,7 +36,9 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { place: number = 0, confidence: number = null, otherInformation: any = null, - metadata: string = null) { + source: string = null, + metadata: string = null + ) { this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null; this.language = language; this.authority = authority; @@ -54,7 +57,7 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { if (isNotEmpty(metadata)) { this.metadata = metadata; } - + this.source = source; this.otherInformation = otherInformation; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 766b502d657..7cc67903ffb 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2461,6 +2461,8 @@ "form.other-information.ror_orgunit_acronym": "ROR acronym", + "form.entry.source": "Source", + "form.remove": "Remove", "form.save": "Save", From 35722fd1c2e22d16672d4670a5292bcf982be71d Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Wed, 31 Jan 2024 10:32:48 +0100 Subject: [PATCH 24/31] add source image mapping, handle error, style onebox component --- .../onebox/dynamic-onebox.component.html | 18 ++++++++++++++---- .../onebox/dynamic-onebox.component.scss | 4 ++++ .../models/onebox/dynamic-onebox.component.ts | 8 ++++++++ src/assets/i18n/en.json5 | 4 ++++ src/assets/images/source-local.png | Bin 0 -> 4460 bytes src/assets/images/source-orcid.png | Bin 0 -> 1068 bytes 6 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/assets/images/source-local.png create mode 100644 src/assets/images/source-orcid.png diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html index c630bedc5fa..2f5ae126487 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html @@ -7,20 +7,30 @@
    -
  • {{entry.value}}
  • +
  • +
    + +
    +
    {{entry.value}}
    +
    {{ ('form.entry.source.' + entry.source) | translate}}
    +
  • {{ 'form.other-information.' + item.key | translate }} : {{item.value !== '' ? getOtherInfoValue(item.value) : ('form.other-information.not-available' | translate)}}
  • -
  • {{ 'form.entry.source' | translate}} : {{entry.source}}
    -
  • {{entry.value}}
  • -
  • {{ 'form.entry.source' | translate }} : {{entry.source}}
  • +
  • +
    + +
    +
    {{entry.value}}
    +
    {{ ('form.entry.source.' + entry.source) | translate}}
    +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss index 6e9b796bb90..bacf1267820 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss @@ -40,3 +40,7 @@ background-color: #fff; cursor: pointer; } + +.list-item img { + height: 20px +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 1e7e0e14290..e026e424511 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -311,4 +311,12 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); } + + /** + * Hide image on error + * @param image + */ + handleImgError(image: HTMLElement): void { + image.style.display = 'none'; + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 7cc67903ffb..a8cf69aa0ad 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2463,6 +2463,10 @@ "form.entry.source": "Source", + "form.entry.source.local": "- Local", + + "form.entry.source.orcid": "- Orcid", + "form.remove": "Remove", "form.save": "Save", diff --git a/src/assets/images/source-local.png b/src/assets/images/source-local.png new file mode 100644 index 0000000000000000000000000000000000000000..dc3c552056465abc8026e0eab51988b5ecee064a GIT binary patch literal 4460 zcmeHJX*`r&8=sM_u`ds0i?Nn1j6E^T*mv1uBFxyAs0bse87g~ZER!vjm?SOMibApu zQ9@*mq3qkty!SjG-rxIvc)q-!-}`*HuXE1-e{J`5pX;RB+nRAfL?9p#h|9tp;RpgT z0U{HK{Rr@Ce<;%hOpNTU&mn>7|DXS7;D5^iolYlzu5|~m`kMMTBKB~D06L8WD65P$b2mLBGb=kM_g?<}0zzR?3GqQ` zS$Rd}!>UJ*pHx4ssUbItCg{BW zS8nufX*w4|gcf%1C}BIAh$E9y+iuZKmwVRv(p3ykVy&cgJ73;K#h!v$TMNa~map^*^?z*f zOqtvXy9^7y;7Y3w%PH+y6Znhkhtu>^L-qk`U;pC5J{2i7w2_>?oDGE8!@F-l12vEG*XR-82XpEd0y3uP`gZCb zFz#Qapz3bq7W&=vD?Bm-M`}5jbjopXcEAb+mJ#rpZ?*@u9;vVn-Jz<=@`VaTD}vhS z&I=)<)HbBVesqgybgs_k?IVxz}<|QVMg~C{c>J*Ooo} zrc;eA;V8UbZrbeK6P7s%Dam^%&qw3{59+^V*Qlyg z_U`ElzI_%|=To@B*mYV_TIE_;0JaGUmxUIBV;D|^5XT&sHi3}FEU(>GjAkq;O-;@S z=^5adj^$kpkNjC&Vgly0s(nx|ehXlbGy5G!}c%#gE9vnNTs|>+zhk z7bUfmU-Qjs9Z-~4lgJF4HGFY+@~3vmd_oX9l3YXtRE+}?N(V^Bu`hYP3E>HevP#AP zH1OsZ;hn5hz~j@)s0G_lH8mhBF1|65g`a^iZy6B$Lc?=T6cm_%t?PHo`%Ik}99+%e z(*yh)fb@d&eyVBQCkpUid8OLxWnnP@thS{Dhm*wAfLQKdew-1!+nwLrUkZ9?pkha% zuw?L*12&G09{TWTZ#cTVLIeZkTFQoYlfmeY&G=<$#;1)PJ;iZEV2EFUEXNE{Bq6cf zETI1yZ|j*}oHZM$Hg$)ot;c1`QYwO_V#-E}-s<@%Ruq z_8KibzAHX?;FK!@!5@~HV*9RYVR!`!DQ->NAOFdMD0P}7Gj(WZ-86O8Ls{;q`C!g{ z-=n3j@CGglh-@5k$xI30&f*%IlLNFO7d2Xzt?+$WI4yKz%Z(7e>lh8vf#;%a7Jl9k z1bu@j>sb*-A>83|1hK12HWdYj)^1q`7sbz{AFB%wwKL;8v{mtd$!u+Etsno+>r$2w z*S}VdGK0m@!FYU!xV$Kr?RK2j*JnaQe_Fpxw-7CIdaQn)QH8aUoF2dCw7&-ay@}y( zmHZWXEy0Id?x~!#s=mmHRm{-4DvR!>zRDECqTLa_sP!itfiCItCt|WH&NR}qe7grU z{N}pb=u5s1>pT^6@0Q#A>OQySG&j$WGuORy{ag;W3q&moT#G8S=+&=Pnv&$~`&Pk@i= zX?jAXBJva_7C*OKi*0wrn&WPmfKEL>Nz+ z_7&WTjH$dN&|%R&IT=ql!QWX?SoJB-AJ2D@=|rwe(W77p&~q?N0wGWI9UK~=r#=3m zcm30H((T=-Ji%P&kzsANH5yK#Ni?LE^!VGkXF8o|zdh+m>eFrRy3Vrk2G6{Vi0D0w zJx`Ht-ItoVy>`(72Y)!OOv#|M*;gSQX7SYWFNY@2R|n5$3k+`cKM9gQcjc(Kr%2tF z>fYTm6N}%=@J|ib#2gw&zJ3Rju)7kz(e>r-N1E1AF@>C7iNr@35D|i=!jo37B$^tI zuBk$@N`#|xzcKZ(=*Ql6to~Yt|$+?Iwgh(97=A4z&+qzFsFMEtIsFc6_WBd6<3cn#N>ZMOE_{qXjDDj( zJ3C*TW5Z8!mG^k;_?5^6p$i5_W4$6#2Ry%bJ2#B?TGkA64(O2c%Uj)6HlpRI+v9uhx6WtNER%r zFWhAMEiUOF)U<{@nTn}@;0fNPlJw9`t90e{fvV++2jv~5c3T_Ng$Mz+(t)u)rg&o! zk(%XrNW{IJ=@G0J#w)(^sLp5_jJr-iT*%_kD3xXmP>d9_g?a0R$ zD8pX!3yCnzyy0B{U6)W>+)Bsi3f%DahdNqu#uW?w`J?XkA#Bf2>?B6ZBab&Z0DMxC zx!1+~xD>|a6ysbuvd#gVHpW*Ki-KZ?tl0^gM-3q^$_hKcF@wD+4R;O}Rv)+voYnt1 zIE{K*@9q7>$;ohv6WgM&Rx7>bAlo2C;t+K!*y+{F4?|}lKOQMo$LSPlR6aZDGVcH~ zGdQ(2XZ>=vjIHi;29r*8r0*K~PczMRbHM?Grjq)lk2~?0U6Wvm__Dg&%Hy|*Jw6dD z_||>Ao!H%^N!U>{9Z>{MMy42%J7ko82lSr+awpmVb+yJGXx$$8}Vg_KtOR8ig@ON_Q!5k9&@R$qbtw(ucnv-owFFG)DT~KP(^us$K$8 z>uEzk{Zr)+`>jd|vP$m(C>c_tx>An$Tn^%{r)jPZ=P^WE#k$64HiHEKZ5xx_kYJPe zcld!q*w0^&)d0~kT3gY9`6du_Rl;Ail8JOoA3)KT#CPqwtDOGQyAd< z{nwX?@w)pAa}xPg+uOdKxWB(s?OwY?*i7Zqu+)11E;5wZ_LUOb8vgC2XdMi2bj`!x z=Yp|^ukW`MBcHj3u|hMj(Z7cRNCC7d^q`m%c1?*s6$#IXl7Gbanw5QN!hE#7<~Yy$v9e`~;E%+-I6>AaN6$sl)s?ysLnFoZ4c810oM7 z?;eCq&avTNUA%mOlE4puK=^6EF%578VqPG{u6g z6t9;ENw%gc!5WA5)EhI7nv~D`_ZPw?_DXa%b%*ChH0o1Lh)Z6mv(f2i zUg(l36Blo@BMM_Drp{hTL%`@@-fmfbuYk5)rcV&{!P&P~;gY?x==W*gPb|Mx>xjr` zFc63T)_;Q;%p4|rcA+Co+)}rf4BBvs;)9D+Hc2S|m7)-s&e!DvzjhDv&I1qJPNI;F zit62s=7Gaj+wvK8BMQq~vb4o+@}o}5)=k~aXM{V=E13A#(iUt)IoUP6e14k!jO%h& zceP4|yKY|mFfx06k!F@AYto;2Po7Szx{d5M%*%HRY{)S#-I^hOA?N%OzBozcCJNE_3(%hIVOV=SX%) zDhx}rom5zYgzC@lec(G9E2@iD|2T55&xTi?>)N%Az(jMgoUoEpo_SKHcaFx}dqkGi tp1g`1E2r2#d%TK3E~UN2fd8S5O>SbHd8!RyoJ*Pvgay(TQEPl5`CsE*AZh>r literal 0 HcmV?d00001 diff --git a/src/assets/images/source-orcid.png b/src/assets/images/source-orcid.png new file mode 100644 index 0000000000000000000000000000000000000000..189bfbe0bcca818c4b8bed002bef270917a8f9dc GIT binary patch literal 1068 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817lu*Plzi}!IBf^%g$LY zJ7={NM1s*-i>2qR<{q(Lals5I0u)~kB+r|#ItG-r0I4}^Ie)(ckX(MwV(}T<v#q#q&1yDhV)Y9{otFAgMJ#7QwZoBOXQLyTQ<&ra&tIpX% z#Fw21*}m*7$kMf!JXc_}%-ulqp^8f$;|NQfL#d*8Ew{tdM4c%}h zVBICJ)fb#MT@BuJC3w{Zr%hKw-h91w@?qPrKc64I-@E5l=KF8AZ@=Df@9oy7ACG?j z_2$;=wbx!O2HJk`Zt1sQuWr0tvGGdKkKZ5u|NpoBdfbJlQ*Xasx8+*ohwt~+Ty*>X z``xzdu^+!bIQwMc-8Wml|9-pcX4=+kQFq^Le);*_wUFrj?7Q!F-nvWPZ`0RQiu zkM$%joN4?#?MwK~*Drtmm=VgO)P9Wp(Nk$%(SvO#riYz8u_6{|24j-9yNkoWn=vYWU)BAqT&AD4Yu4?S3=-kEHt%?y+G@p!N58F>Tt4z7WZlFUCCLYx`7dXrg-up% z+3&xuJ})lXZTgfWe&2Ssx^hgPrX3X-+EMWI-;>{w&aVaX6xN*mGmWRfETKVbL4Z!) z`!`Bk)Wc;M-Ap&=zx8Hsf6>vh_<#$ef{mS}t@3xyT`L(xE?UMVH!Nose<8Hqw(5ZB zu4#;OMa*A>`8nzf-CJ0_(}P>|gv|n@dq*0lve}4T5a#^O)ELZoev!t$J^L2^vEG@* z$guod{DS`TLY+}d>XJT~{r<}P@cjBEpW?r!f8t!JxF;aIcFEH}Rdw}~OjKtt`pw*Y zbsHaBMqQSNWY0svQ@?Niy6DwrvGr`fn)I^Cyw??DOdMzCD+>7^udtgYCoMkZh5hr{ zdy1@6b?yOUMYY5=q9i4;B-JXpC>2OC7#SE^>KYj98d!!Hnp+u|SQ(mY8yHv_7~8glbfGSez?Yq0(i(g4)J;OXk;vd$@?2>_(}1K9uo literal 0 HcmV?d00001 From 91881896ef621210974eade7d6ea53d1b326e8d6 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 1 Feb 2024 16:43:16 +0100 Subject: [PATCH 25/31] DSC-1498: update source labels --- src/assets/i18n/en.json5 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a8cf69aa0ad..771d6ffeacc 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2467,6 +2467,14 @@ "form.entry.source.orcid": "- Orcid", + "form.entry.source.ror": "- ROR", + + "form.entry.source.openaire": "- OpenAIRE", + + "form.entry.source.zdb": "- ZDB", + + "form.entry.source.sherpa": "- Sherpa", + "form.remove": "Remove", "form.save": "Save", From b58ed3dd4d7ba93a75875d281af3073cbf8c1900 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 2 Feb 2024 09:34:43 +0100 Subject: [PATCH 26/31] DSC-1489 update mapped icon and labels --- .../models/onebox/dynamic-onebox.component.html | 5 ++--- src/assets/i18n/en.json5 | 14 ++++++-------- src/assets/images/local.logo.icon.svg | 1 + src/assets/images/openaire.logo.icon.svg | 1 + src/assets/images/ror.logo.icon.svg | 13 +++++++++++++ src/assets/images/source-local.png | Bin 4460 -> 0 bytes src/assets/images/source-orcid.png | Bin 1068 -> 0 bytes src/assets/images/zdb.logo.icon.svg | 1 + 8 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 src/assets/images/local.logo.icon.svg create mode 100644 src/assets/images/openaire.logo.icon.svg create mode 100644 src/assets/images/ror.logo.icon.svg delete mode 100644 src/assets/images/source-local.png delete mode 100644 src/assets/images/source-orcid.png create mode 100644 src/assets/images/zdb.logo.icon.svg diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html index 2f5ae126487..bc98b84464b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html @@ -9,7 +9,7 @@
  • - +
    {{entry.value}}
    {{ ('form.entry.source.' + entry.source) | translate}}
    @@ -26,7 +26,7 @@
    • - +
      {{entry.value}}
      {{ ('form.entry.source.' + entry.source) | translate}}
      @@ -91,7 +91,6 @@ [disabled]="model.readOnly" [type]="model.inputType" [value]="currentValue?.display" - [disabled]="model.readOnly" (focus)="onFocus($event)" (change)="onChange($event)" (click)="openTree($event)" diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 771d6ffeacc..35beb812a03 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2461,19 +2461,17 @@ "form.other-information.ror_orgunit_acronym": "ROR acronym", - "form.entry.source": "Source", + "form.entry.source.local": "", - "form.entry.source.local": "- Local", + "form.entry.source.orcid": "", - "form.entry.source.orcid": "- Orcid", + "form.entry.source.ror": "", - "form.entry.source.ror": "- ROR", + "form.entry.source.openaire": "", - "form.entry.source.openaire": "- OpenAIRE", + "form.entry.source.zdb": "", - "form.entry.source.zdb": "- ZDB", - - "form.entry.source.sherpa": "- Sherpa", + "form.entry.source.sherpa": "", "form.remove": "Remove", diff --git a/src/assets/images/local.logo.icon.svg b/src/assets/images/local.logo.icon.svg new file mode 100644 index 00000000000..7d1208f1f73 --- /dev/null +++ b/src/assets/images/local.logo.icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/openaire.logo.icon.svg b/src/assets/images/openaire.logo.icon.svg new file mode 100644 index 00000000000..ea4d2014d5b --- /dev/null +++ b/src/assets/images/openaire.logo.icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/ror.logo.icon.svg b/src/assets/images/ror.logo.icon.svg new file mode 100644 index 00000000000..6bea5c60952 --- /dev/null +++ b/src/assets/images/ror.logo.icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/images/source-local.png b/src/assets/images/source-local.png deleted file mode 100644 index dc3c552056465abc8026e0eab51988b5ecee064a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4460 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 diff --git a/src/assets/images/source-orcid.png b/src/assets/images/source-orcid.png deleted file mode 100644 index 189bfbe0bcca818c4b8bed002bef270917a8f9dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1068 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0817lu*Plzi}!IBf^%g$LY zJ7={NM1s*-i>2qR<{q(Lals5I0u)~kB+r|#ItG-r0I4}^Ie)(ckX(MwV(}T<v#q#q&1yDhV)Y9{otFAgMJ#7QwZoBOXQLyTQ<&ra&tIpX% z#Fw21*}m*7$kMf!JXc_}%-ulqp^8f$;|NQfL#d*8Ew{tdM4c%}h zVBICJ)fb#MT@BuJC3w{Zr%hKw-h91w@?qPrKc64I-@E5l=KF8AZ@=Df@9oy7ACG?j z_2$;=wbx!O2HJk`Zt1sQuWr0tvGGdKkKZ5u|NpoBdfbJlQ*Xasx8+*ohwt~+Ty*>X z``xzdu^+!bIQwMc-8Wml|9-pcX4=+kQFq^Le);*_wUFrj?7Q!F-nvWPZ`0RQiu zkM$%joN4?#?MwK~*Drtmm=VgO)P9Wp(Nk$%(SvO#riYz8u_6{|24j-9yNkoWn=vYWU)BAqT&AD4Yu4?S3=-kEHt%?y+G@p!N58F>Tt4z7WZlFUCCLYx`7dXrg-up% z+3&xuJ})lXZTgfWe&2Ssx^hgPrX3X-+EMWI-;>{w&aVaX6xN*mGmWRfETKVbL4Z!) z`!`Bk)Wc;M-Ap&=zx8Hsf6>vh_<#$ef{mS}t@3xyT`L(xE?UMVH!Nose<8Hqw(5ZB zu4#;OMa*A>`8nzf-CJ0_(}P>|gv|n@dq*0lve}4T5a#^O)ELZoev!t$J^L2^vEG@* z$guod{DS`TLY+}d>XJT~{r<}P@cjBEpW?r!f8t!JxF;aIcFEH}Rdw}~OjKtt`pw*Y zbsHaBMqQSNWY0svQ@?Niy6DwrvGr`fn)I^Cyw??DOdMzCD+>7^udtgYCoMkZh5hr{ zdy1@6b?yOUMYY5=q9i4;B-JXpC>2OC7#SE^>KYj98d!!Hnp+u|SQ(mY8yHv_7~8glbfGSez?Yq0(i(g4)J;OXk;vd$@?2>_(}1K9uo diff --git a/src/assets/images/zdb.logo.icon.svg b/src/assets/images/zdb.logo.icon.svg new file mode 100644 index 00000000000..a495cc2718f --- /dev/null +++ b/src/assets/images/zdb.logo.icon.svg @@ -0,0 +1 @@ +180509 ZDB-Logo From 2e532e8fb3db50239e9ffb519204ad6bb2b3cdd6 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Thu, 8 Feb 2024 16:37:33 +0100 Subject: [PATCH 27/31] [DSC-1498] configured label for Sherpa Romeo source in authority dropdown --- src/assets/i18n/en.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 35beb812a03..80565012d63 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2471,7 +2471,7 @@ "form.entry.source.zdb": "", - "form.entry.source.sherpa": "", + "form.entry.source.sherpa": "- Sherpa Romeo", "form.remove": "Remove", From 850554fe9ab0b0583167a1cebc7e1e30b6ebb1e0 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 8 Feb 2024 17:17:18 +0100 Subject: [PATCH 28/31] fix tests --- .../metric/metric-altmetric/metric-altmetric.component.spec.ts | 3 ++- .../metric-dimensions/metric-dimensions.component.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts index d9c64fc09ef..3276c5b5852 100644 --- a/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts +++ b/src/app/shared/metric/metric-altmetric/metric-altmetric.component.spec.ts @@ -42,6 +42,7 @@ describe('MetricAltmetricComponent', () => { component = fixture.componentInstance; component.metric = metricMock; component.success = true; + component.canLoadScript = true; component.maxRetry = 0; fixture.detectChanges(); }); @@ -50,7 +51,7 @@ describe('MetricAltmetricComponent', () => { expect(component).toBeTruthy(); }); it('should render badge div', () => { - const div = fixture.debugElement.queryAll(By.css('div'))[2]; + const div = fixture.debugElement.queryAll(By.css('div'))[3]; expect(div.nativeElement.className).toEqual('altmetric-embed'); expect(div.nativeElement.dataset.badgePopover).toEqual('bottom'); expect(div.nativeElement.dataset.doi).toEqual('10.1056/Test'); diff --git a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts index 5c8397989ea..f2b7a9fe378 100644 --- a/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts +++ b/src/app/shared/metric/metric-dimensions/metric-dimensions.component.spec.ts @@ -41,6 +41,7 @@ describe('MetricDimensionsComponent', () => { component = fixture.componentInstance; component.metric = metricMock; component.success = true; + component.canLoadScript = true; component.maxRetry = 0; fixture.detectChanges(); }); @@ -49,7 +50,7 @@ describe('MetricDimensionsComponent', () => { expect(component).toBeTruthy(); }); it('should render badge div', () => { - const div = fixture.debugElement.queryAll(By.css('div'))[1]; + const div = fixture.debugElement.queryAll(By.css('div'))[2]; expect(div.nativeElement.className).toEqual('__dimensions_badge_embed__'); expect(div.nativeElement.dataset.doi).toEqual('10.1056/Test'); expect(div.nativeElement.dataset.style).toEqual('small_rectangle'); From e2cec018988eeca6822b83b403cdebd5d6d7fb3f Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 8 Feb 2024 17:39:54 +0100 Subject: [PATCH 29/31] DSC-1498 add sherpa logo --- src/assets/images/sherpa.logo.icon.svg | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/assets/images/sherpa.logo.icon.svg diff --git a/src/assets/images/sherpa.logo.icon.svg b/src/assets/images/sherpa.logo.icon.svg new file mode 100644 index 00000000000..e6a4921f3e6 --- /dev/null +++ b/src/assets/images/sherpa.logo.icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + From 8a37ae7014113838c3c6d73e47666495105c5392 Mon Sep 17 00:00:00 2001 From: PoYuan Date: Tue, 30 Apr 2024 13:26:55 +0800 Subject: [PATCH 30/31] Fix missing scope parameter issue --- .../search/item-export/item-export.service.ts | 13 +++++++++++++ .../item-export-list/item-export-list.component.ts | 4 ++++ .../item-export/item-export.component.ts | 1 + 3 files changed, 18 insertions(+) diff --git a/src/app/shared/search/item-export/item-export.service.ts b/src/app/shared/search/item-export/item-export.service.ts index b574be2dd85..252b63e7329 100644 --- a/src/app/shared/search/item-export/item-export.service.ts +++ b/src/app/shared/search/item-export/item-export.service.ts @@ -102,4 +102,17 @@ export class ItemExportService { }; } + /** + * Get the UUID from a searchOptions + * @param searchOptions + */ + public getScopeUUID(searchOptions: SearchOptions): string { + if (searchOptions.fixedFilter) { + const fixedFilter = searchOptions.fixedFilter.split('='); + if (fixedFilter.length === 2 && fixedFilter[0] === 'scope') { + return fixedFilter[1]; + } + } + return null; + } } diff --git a/src/app/shared/search/item-export/item-export/item-export-list/item-export-list.component.ts b/src/app/shared/search/item-export/item-export/item-export-list/item-export-list.component.ts index e2d916638f1..d49362474d0 100644 --- a/src/app/shared/search/item-export/item-export/item-export-list/item-export-list.component.ts +++ b/src/app/shared/search/item-export/item-export/item-export-list/item-export-list.component.ts @@ -15,6 +15,8 @@ import { fadeIn } from '../../../../animations/fade'; import { PaginatedSearchOptions } from '../../../models/paginated-search-options.model'; import { UUIDService } from '../../../../../core/shared/uuid.service'; +import { ItemExportService } from '../../item-export.service'; + @Component({ selector: 'ds-item-export-list', templateUrl: './item-export-list.component.html', @@ -64,6 +66,7 @@ export class ItemExportListComponent implements OnInit { resultsRD$: BehaviorSubject>> = new BehaviorSubject(null); constructor( + protected itemExportService: ItemExportService, private paginationService: PaginationService, private searchManager: SearchManager, private uuidService: UUIDService) { @@ -74,6 +77,7 @@ export class ItemExportListComponent implements OnInit { this.currentPagination$ = this.paginationService.getCurrentPagination(this.initialPagination.id, this.initialPagination); this.currentPagination$.subscribe((paginationOptions: PaginationComponentOptions) => { this.searchOptions = Object.assign(new PaginatedSearchOptions({}), this.searchOptions, { + scope: this.itemExportService.getScopeUUID(this.searchOptions), fixedFilter: `f.entityType=${this.itemEntityType},equals`, pagination: paginationOptions }); diff --git a/src/app/shared/search/item-export/item-export/item-export.component.ts b/src/app/shared/search/item-export/item-export/item-export.component.ts index 6b7640ea148..c81f38104c8 100644 --- a/src/app/shared/search/item-export/item-export/item-export.component.ts +++ b/src/app/shared/search/item-export/item-export/item-export.component.ts @@ -260,6 +260,7 @@ export class ItemExportComponent implements OnInit, OnDestroy { private canExport(): Observable { return this.searchManager.search( Object.assign(new PaginatedSearchOptions({}), this.searchOptions, { + scope: this.itemExportService.getScopeUUID(this.searchOptions), fixedFilter: `f.entityType=${this.itemType.label},equals`, pagination: Object.assign(new PaginationComponentOptions(), { id: this.uuidService.generate(), From bd1da6b4c94e84ce93d5748ecc80bea67aefaf8f Mon Sep 17 00:00:00 2001 From: PoYuan Date: Tue, 30 Apr 2024 13:42:08 +0800 Subject: [PATCH 31/31] Remove Trailing spaces --- src/app/shared/search/item-export/item-export.service.ts | 2 +- .../search/item-export/item-export/item-export.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/search/item-export/item-export.service.ts b/src/app/shared/search/item-export/item-export.service.ts index 252b63e7329..3c5d595b2cc 100644 --- a/src/app/shared/search/item-export/item-export.service.ts +++ b/src/app/shared/search/item-export/item-export.service.ts @@ -103,7 +103,7 @@ export class ItemExportService { } /** - * Get the UUID from a searchOptions + * Get the UUID from a searchOptions * @param searchOptions */ public getScopeUUID(searchOptions: SearchOptions): string { diff --git a/src/app/shared/search/item-export/item-export/item-export.component.ts b/src/app/shared/search/item-export/item-export/item-export.component.ts index c81f38104c8..c5f5f47ce3b 100644 --- a/src/app/shared/search/item-export/item-export/item-export.component.ts +++ b/src/app/shared/search/item-export/item-export/item-export.component.ts @@ -260,7 +260,7 @@ export class ItemExportComponent implements OnInit, OnDestroy { private canExport(): Observable { return this.searchManager.search( Object.assign(new PaginatedSearchOptions({}), this.searchOptions, { - scope: this.itemExportService.getScopeUUID(this.searchOptions), + scope: this.itemExportService.getScopeUUID(this.searchOptions), fixedFilter: `f.entityType=${this.itemType.label},equals`, pagination: Object.assign(new PaginationComponentOptions(), { id: this.uuidService.generate(),