diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index b9b89725d5b..9cd6e3b3211 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -10,25 +10,11 @@ - + - + diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts index ae07babdfc0..69387e75346 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.spec.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -10,30 +10,22 @@ import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; import { RouterTestingModule } from '@angular/router/testing'; import { of as observableOf } from 'rxjs'; import { DebugElement } from '@angular/core'; -import { BreadcrumbTooltipPipe } from './breadcrumb/breadcrumb-tooltip.pipe'; -import { TruncateBreadcrumbItemCharactersPipe } from './breadcrumb/truncate-breadcrumb-item-characters.pipe'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; describe('BreadcrumbsComponent', () => { let component: BreadcrumbsComponent; let fixture: ComponentFixture; let breadcrumbsServiceMock: BreadcrumbsService; - let truncateTextPipe: TruncateBreadcrumbItemCharactersPipe; const expectBreadcrumb = (listItem: DebugElement, text: string, url: string) => { const anchor = listItem.query(By.css('a')); - const truncatedText = truncateTextPipe.transform(text); + if (url == null) { expect(anchor).toBeNull(); - // remove leading whitespace characters - const textWithoutSpaces = listItem.nativeElement.innerHTML.trimStart().replace(/^\s+/, ''); - expect(textWithoutSpaces).toEqual(truncatedText); + expect(listItem.nativeElement.innerHTML).toEqual(text); } else { expect(anchor).toBeInstanceOf(DebugElement); expect(anchor.attributes.href).toEqual(url); - // remove leading whitespace characters - const textWithoutSpaces = anchor.nativeElement.innerHTML.trimStart().replace(/^\s+/, ''); - expect(textWithoutSpaces).toEqual(truncatedText); + expect(anchor.nativeElement.innerHTML).toEqual(text); } }; @@ -43,7 +35,6 @@ describe('BreadcrumbsComponent', () => { // NOTE: a root breadcrumb is automatically rendered new Breadcrumb('bc 1', 'example.com'), new Breadcrumb('bc 2', 'another.com'), - new Breadcrumb('breadcrumb to be truncated', 'truncated.com'), ]), showBreadcrumbs$: observableOf(true), } as BreadcrumbsService; @@ -52,11 +43,8 @@ describe('BreadcrumbsComponent', () => { declarations: [ BreadcrumbsComponent, VarDirective, - BreadcrumbTooltipPipe, - TruncateBreadcrumbItemCharactersPipe, ], imports: [ - NgbTooltipModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ loader: { @@ -67,12 +55,10 @@ describe('BreadcrumbsComponent', () => { ], providers: [ { provide: BreadcrumbsService, useValue: breadcrumbsServiceMock }, - { provide: TruncateBreadcrumbItemCharactersPipe, useClass: TruncateBreadcrumbItemCharactersPipe }, ], }).compileComponents(); fixture = TestBed.createComponent(BreadcrumbsComponent); - truncateTextPipe = TestBed.inject(TruncateBreadcrumbItemCharactersPipe); component = fixture.componentInstance; fixture.detectChanges(); })); @@ -81,35 +67,12 @@ describe('BreadcrumbsComponent', () => { expect(component).toBeTruthy(); }); - it('should render the breadcrumbs accordingly', () => { + it('should render the breadcrumbs', () => { const breadcrumbs = fixture.debugElement.queryAll(By.css('.breadcrumb-item')); - expect(breadcrumbs.length).toBe(4); + expect(breadcrumbs.length).toBe(3); expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/'); expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com'); expectBreadcrumb(breadcrumbs[2].query(By.css('.text-truncate')), 'bc 2', null); - expectBreadcrumb(breadcrumbs[3].query(By.css('.text-truncate')), 'breadcrumb...', null); - }); - - it('should show tooltip only for truncated text', () => { - const breadcrumbs = fixture.debugElement.queryAll(By.css('.breadcrumb-item .text-truncate')); - expect(breadcrumbs.length).toBe(4); - - const truncatable = breadcrumbs[3]; - truncatable.triggerEventHandler('mouseenter', null); - fixture.detectChanges(); - let tooltip = truncatable.parent.query(By.css('div.tooltip-inner')); - expect(tooltip).not.toBeNull(); - expect(tooltip.nativeElement.innerText).toBe('breadcrumb to be truncated'); - truncatable.triggerEventHandler('mouseleave', null); - fixture.detectChanges(); - - const notTruncatable = breadcrumbs[2]; - notTruncatable.triggerEventHandler('mouseenter', null); - fixture.detectChanges(); - const tooltip2 = notTruncatable.parent.query(By.css('div.tooltip-inner')); - expect(tooltip2).toBeNull(); - notTruncatable.triggerEventHandler('mouseleave', null); - fixture.detectChanges(); }); }); diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 6b84301e277..3949b2671f8 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -74,7 +74,7 @@

{{'collection.page.browse.recent.head' | translate}}

- diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index b55e1d722ee..d1c4c93e7c5 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, Inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs'; import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators'; @@ -62,6 +63,7 @@ export class CollectionPageComponent implements OnInit { collectionPageRoute$: Observable; constructor( + @Inject(PLATFORM_ID) private platformId: Object, private collectionDataService: CollectionDataService, private searchService: SearchService, private route: ActivatedRoute, @@ -82,6 +84,10 @@ export class CollectionPageComponent implements OnInit { } ngOnInit(): void { + if (isPlatformServer(this.platformId)) { + return; + } + this.collectionRD$ = this.route.data.pipe( map((data) => data.dso as RemoteData), redirectOn4xx(this.router, this.authService), diff --git a/src/app/cris-item-page/cris-item-page-routing.module.ts b/src/app/cris-item-page/cris-item-page-routing.module.ts deleted file mode 100644 index a67d4f5fd16..00000000000 --- a/src/app/cris-item-page/cris-item-page-routing.module.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { CrisItemPageTabResolver } from './../item-page/cris-item-page-tab.resolver'; -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { CrisItemPageResolver } from './cris-item-page.resolver'; -import { CrisItemPageComponent } from './cris-item-page.component'; -import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; -import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; - -const routes: Routes = [ - { - path: ':id', - component: CrisItemPageComponent, - resolve: { - dso: CrisItemPageResolver, - breadcrumb: ItemBreadcrumbResolver, - tabs: CrisItemPageTabResolver - }, - data: { - menu: { - public: [{ - id: 'statistics_item_:id', - active: true, - visible: false, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/items/:id/', - } as LinkMenuItemModel, - }], - } - , showSocialButtons: true - } - }, - { // used for activate specific tab - path: ':id/:tab', - component: CrisItemPageComponent, - resolve: { - dso: CrisItemPageResolver, - breadcrumb: ItemBreadcrumbResolver, - tabs: CrisItemPageTabResolver - }, - data: { - menu: { - public: [{ - id: 'statistics_item_:id', - active: true, - visible: false, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/items/:id/', - } as LinkMenuItemModel, - }], - }, showSocialButtons: true - } - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], - providers: [ - CrisItemPageResolver, - ItemBreadcrumbResolver - ] -}) -export class CrisItemPageRoutingModule { } diff --git a/src/app/cris-item-page/cris-item-page.module.ts b/src/app/cris-item-page/cris-item-page.module.ts index e5c63541877..18274bdfeeb 100644 --- a/src/app/cris-item-page/cris-item-page.module.ts +++ b/src/app/cris-item-page/cris-item-page.module.ts @@ -1,4 +1,3 @@ -import { CrisItemPageRoutingModule } from './cris-item-page-routing.module'; import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; @@ -17,7 +16,6 @@ import { ItemSharedModule } from '../item-page/item-shared.module'; SharedModule, CrisLayoutModule, StatisticsModule, - CrisItemPageRoutingModule, ItemSharedModule ], exports: [ diff --git a/src/app/cris-layout/cris-layout-loader/shared/cris-layout-tabs/cris-layout-tabs.component.ts b/src/app/cris-layout/cris-layout-loader/shared/cris-layout-tabs/cris-layout-tabs.component.ts index 500f34fcf8a..34037f93889 100644 --- a/src/app/cris-layout/cris-layout-loader/shared/cris-layout-tabs/cris-layout-tabs.component.ts +++ b/src/app/cris-layout/cris-layout-loader/shared/cris-layout-tabs/cris-layout-tabs.component.ts @@ -108,10 +108,13 @@ export abstract class CrisLayoutTabsComponent { abstract emitSelected(selectedTab): void; setActiveTab(tab) { + const itemPageRoute = getItemPageRoute(this.item); this.activeTab$.next(tab); this.emitSelected(tab); - if (isNotNull(this.route.snapshot.paramMap.get('tab'))) { - this.location.replaceState(getItemPageRoute(this.item) + '/' + tab.shortname); + if (this.tabs[0].shortname === tab.shortname) { + this.location.replaceState(itemPageRoute); + } else { + this.location.replaceState(itemPageRoute + '/' + tab.shortname); } } diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html index d62dfd7beae..e2ab0222037 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html @@ -1,6 +1,7 @@ [] = []; if (this.appConfig.browseBy.showThumbnails) { linksToFollow.push(followLink('thumbnail')); diff --git a/src/app/item-page/cris-item-page-tab.resolver.spec.ts b/src/app/item-page/cris-item-page-tab.resolver.spec.ts new file mode 100644 index 00000000000..c7a422ccec2 --- /dev/null +++ b/src/app/item-page/cris-item-page-tab.resolver.spec.ts @@ -0,0 +1,152 @@ +import { take } from 'rxjs/operators'; +import { ItemDataService } from '../core/data/item-data.service'; +import { Item } from '../core/shared/item.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Router } from '@angular/router'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; +import { CrisItemPageTabResolver } from './cris-item-page-tab.resolver'; +import { TabDataService } from '../core/layout/tab-data.service'; +import { createPaginatedList } from '../shared/testing/utils.test'; +import { tabDetailsTest, tabPublicationsTest } from '../shared/testing/layout-tab.mocks'; + +describe('CrisItemPageTabResolver', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([{ + path: 'entities/:entity-type/:id/:tab', + component: {} as any + }])] + }); + }); + + describe('when item exists', () => { + let resolver: CrisItemPageTabResolver; + const itemService: jasmine.SpyObj = jasmine.createSpyObj('ItemDataService', { + 'findById': jasmine.createSpy('findById') + }); + const tabService: jasmine.SpyObj = jasmine.createSpyObj('TabDataService', { + 'findByItem': jasmine.createSpy('findByItem') + }); + let hardRedirectService: HardRedirectService; + + let router; + + const uuid = '1234-65487-12354-1235'; + const item = Object.assign(new Item(), { + id: uuid, + uuid: uuid, + metadata: { + 'dspace.entity.type': [ + { + value: 'Publication' + } + ] + } + }); + + const tabsRD = createSuccessfulRemoteDataObject(createPaginatedList([tabPublicationsTest, tabDetailsTest])); + const tabsRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([tabPublicationsTest, tabDetailsTest])); + + const noTabsRD = createSuccessfulRemoteDataObject(createPaginatedList([])); + const noTabsRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([])); + + beforeEach(() => { + router = TestBed.inject(Router); + + itemService.findById.and.returnValue(createSuccessfulRemoteDataObject$(item)); + + hardRedirectService = jasmine.createSpyObj('HardRedirectService', { + 'redirect': jasmine.createSpy('redirect') + }); + }); + + describe('and there tabs', () => { + beforeEach(() => { + + (tabService as any).findByItem.and.returnValue(tabsRD$); + + spyOn(router, 'navigateByUrl'); + + resolver = new CrisItemPageTabResolver(hardRedirectService, tabService, itemService, router); + }); + + it('should redirect to root route if given tab is the first one', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/publications' } as any) + .pipe(take(1)) + .subscribe( + (resolved) => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(hardRedirectService.redirect).toHaveBeenCalledWith('/entities/publication/1234-65487-12354-1235', 302); + expect(resolved).toEqual(tabsRD); + done(); + } + ); + }); + + it('should not redirect to root route if tab different than the main one is given', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/details' } as any) + .pipe(take(1)) + .subscribe( + (resolved) => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); + expect(resolved).toEqual(tabsRD); + done(); + } + ); + }); + + it('should not redirect to root route if no tab is given', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235' } as any) + .pipe(take(1)) + .subscribe( + (resolved) => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); + expect(resolved).toEqual(tabsRD); + done(); + } + ); + }); + + it('should navigate to 404 if a wrong tab is given', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235/test' } as any) + .pipe(take(1)) + .subscribe( + (resolved) => { + expect(router.navigateByUrl).toHaveBeenCalled(); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); + expect(resolved).toEqual(tabsRD); + done(); + } + ); + }); + }); + + describe('and there no tabs', () => { + beforeEach(() => { + + (tabService as any).findByItem.and.returnValue(noTabsRD$); + + spyOn(router, 'navigateByUrl'); + + resolver = new CrisItemPageTabResolver(hardRedirectService, tabService, itemService, router); + }); + + it('should not redirect nor navigate', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/publication/1234-65487-12354-1235' } as any) + .pipe(take(1)) + .subscribe( + (resolved) => { + expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); + expect(resolved).toEqual(noTabsRD); + done(); + } + ); + }); + }); + }); +}); diff --git a/src/app/item-page/cris-item-page-tab.resolver.ts b/src/app/item-page/cris-item-page-tab.resolver.ts index e601c75fbdc..aaca1568572 100644 --- a/src/app/item-page/cris-item-page-tab.resolver.ts +++ b/src/app/item-page/cris-item-page-tab.resolver.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; @@ -13,6 +13,8 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Item } from '../core/shared/item.model'; import { getItemPageRoute } from './item-page-routing-paths'; import { createFailedRemoteDataObject$ } from '../shared/remote-data.utils'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; +import { getPageNotFoundRoute } from '../app-routing-paths'; /** * This class represents a resolver that requests the tabs of specific @@ -21,7 +23,11 @@ import { createFailedRemoteDataObject$ } from '../shared/remote-data.utils'; @Injectable() export class CrisItemPageTabResolver implements Resolve>> { - constructor(private tabService: TabDataService, private itemDataService: ItemDataService, private router: Router) { } + constructor( + private hardRedirectService: HardRedirectService, + private tabService: TabDataService, + private itemDataService: ItemDataService, + private router: Router) { } /** * Method for resolving the tabs of item based on the parameters in the current route @@ -45,18 +51,16 @@ export class CrisItemPageTabResolver implements Resolve 0) { // By splitting the url with uuid we can understand if the item is primary item page or a tab const urlSplit = state.url.split(route.params.id); - // If a no or wrong tab is given redirect to the first tab available - if (!!tabsRD.payload && !!tabsRD.payload.page && tabsRD.payload.page.length > 0 && !urlSplit[1]) { - const selectedTab = tabsRD.payload.page.filter((tab) => !tab.leading)[0]; - if (!!selectedTab) { - let tabName = selectedTab.shortname; - - if (tabName.includes('::')) { - tabName = tabName.split('::')[1]; - } - - this.router.navigateByUrl(getItemPageRoute(itemRD.payload) + '/' + tabName); - } + const givenTab = urlSplit[1]; + const itemPageRoute = getItemPageRoute(itemRD.payload); + const isValidTab = tabsRD.payload.page.some((tab) => !givenTab || `/${tab.shortname}` === givenTab); + const mainTab = tabsRD.payload.page.filter((tab) => !tab.leading)[0]; + if (!isValidTab) { + // If wrong tab is given redirect to 404 page + this.router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true, replaceUrl: false }); + } else if (givenTab === `/${mainTab.shortname}`) { + // If first tab is given redirect to root item page + this.hardRedirectService.redirect(itemPageRoute, 302); } } return tabsRD; diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index cfb28240f74..378bcd717dc 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -29,8 +29,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; resolve: { dso: ItemPageResolver, breadcrumb: ItemBreadcrumbResolver, - menu: DSOEditMenuResolver, - tabs: CrisItemPageTabResolver + menu: DSOEditMenuResolver }, runGuardsAndResolvers: 'always', children: [ @@ -38,6 +37,9 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: '', component: ThemedItemPageComponent, pathMatch: 'full', + resolve: { + tabs: CrisItemPageTabResolver + } }, { path: 'full', @@ -63,6 +65,13 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: ORCID_PATH, component: OrcidPageComponent, canActivate: [AuthenticatedGuard, OrcidPageGuard] + }, + { + path: ':tab', + component: ThemedItemPageComponent, + resolve: { + tabs: CrisItemPageTabResolver + }, } ], data: { diff --git a/src/app/item-page/item-page.resolver.spec.ts b/src/app/item-page/item-page.resolver.spec.ts index e9b9a246721..5d91d2177cc 100644 --- a/src/app/item-page/item-page.resolver.spec.ts +++ b/src/app/item-page/item-page.resolver.spec.ts @@ -6,6 +6,7 @@ import { ItemPageResolver } from './item-page.resolver'; import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; describe('ItemPageResolver', () => { beforeEach(() => { @@ -23,6 +24,7 @@ describe('ItemPageResolver', () => { let store; let router; + let hardRedirectService: HardRedirectService ; const uuid = '1234-65487-12354-1235'; const item = Object.assign(new Item(), { id: uuid, @@ -64,8 +66,12 @@ describe('ItemPageResolver', () => { dispatch: {}, }); + hardRedirectService = jasmine.createSpyObj('HardRedirectService', { + 'redirect': jasmine.createSpy('redirect') + }); + spyOn(router, 'navigateByUrl'); - resolver = new ItemPageResolver(itemService, store, router); + resolver = new ItemPageResolver(hardRedirectService, itemService, store, router); }); it('should resolve a an item from from the item with the url redirect', (done) => { @@ -106,7 +112,7 @@ describe('ItemPageResolver', () => { .pipe(first()) .subscribe( (resolved) => { - expect(router.navigateByUrl).not.toHaveBeenCalledWith('/entities/person/customurl/edit'); + expect(hardRedirectService.redirect).not.toHaveBeenCalledWith('/entities/person/customurl/edit'); done(); } ); @@ -126,16 +132,31 @@ describe('ItemPageResolver', () => { dispatch: {}, }); + hardRedirectService = jasmine.createSpyObj('HardRedirectService', { + 'redirect': jasmine.createSpy('redirect') + }); + spyOn(router, 'navigateByUrl'); - resolver = new ItemPageResolver(itemService, store, router); + resolver = new ItemPageResolver(hardRedirectService, itemService, store, router); }); - it('should not call custom url', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'test-url/1234-65487-12354-1235/edit' } as any) + it('should redirect if it has not the new item url', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/items/1234-65487-12354-1235/edit' } as any) + .pipe(first()) + .subscribe( + (resolved) => { + expect(hardRedirectService.redirect).toHaveBeenCalledWith('/entities/person/1234-65487-12354-1235/edit', 301); + done(); + } + ); + }); + + it('should not redirect if it has the new item url', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/person/1234-65487-12354-1235/edit' } as any) .pipe(first()) .subscribe( (resolved) => { - expect(router.navigateByUrl).toHaveBeenCalledWith('/entities/person/1234-65487-12354-1235/edit'); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); done(); } ); diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 6fd9b9435d8..c289bae5dad 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -9,6 +9,7 @@ import { map } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { getItemPageRoute } from './item-page-routing-paths'; import { ItemResolver } from './item.resolver'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; /** * This class represents a resolver that requests a specific item before the route is activated and will redirect to the @@ -17,6 +18,7 @@ import { ItemResolver } from './item.resolver'; @Injectable() export class ItemPageResolver extends ItemResolver { constructor( + protected hardRedirectService: HardRedirectService, protected itemService: ItemDataService, protected store: Store, protected router: Router @@ -53,7 +55,7 @@ export class ItemPageResolver extends ItemResolver { if (!thisRoute.startsWith(itemRoute)) { const itemId = rd.payload.uuid; const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); - this.router.navigateByUrl(itemRoute + subRoute); + this.hardRedirectService.redirect(itemRoute + subRoute, 301); } } } diff --git a/src/app/openaire/openaire.module.ts b/src/app/openaire/openaire.module.ts index b3a9dcd8ff4..91767a51439 100644 --- a/src/app/openaire/openaire.module.ts +++ b/src/app/openaire/openaire.module.ts @@ -51,7 +51,8 @@ const MODULES = [ CoreModule.forRoot(), StoreModule.forFeature('openaire', openaireReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(openaireEffects), - TranslateModule + TranslateModule, + SearchModule ]; const COMPONENTS = [ @@ -86,7 +87,6 @@ const PROVIDERS = [ @NgModule({ imports: [ ...MODULES, - SearchModule ], declarations: [ ...COMPONENTS, @@ -96,9 +96,6 @@ const PROVIDERS = [ providers: [ ...PROVIDERS ], - entryComponents: [ - ...ENTRY_COMPONENTS - ], exports: [ ...COMPONENTS, ...DIRECTIVES diff --git a/src/app/openaire/reciter-suggestions/selectors.ts b/src/app/openaire/reciter-suggestions/selectors.ts index d5db946817b..6b4b97cdd8b 100644 --- a/src/app/openaire/reciter-suggestions/selectors.ts +++ b/src/app/openaire/reciter-suggestions/selectors.ts @@ -1,16 +1,10 @@ -import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { createSelector, MemoizedSelector } from '@ngrx/store'; import { subStateSelector } from '../../shared/selector.util'; import { openaireSelector, OpenaireState } from '../openaire.reducer'; -import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; -import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; - -/** - * Returns the Reciter Suggestion Target state. - * @function _getReciterSuggestionTargetState - * @param {AppState} state Top level state. - * @return {OpenaireState} - */ -const _getReciterSuggestionTargetState = createFeatureSelector('openaire'); +import { + OpenaireSuggestionTarget +} from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTargetEntry, SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; // Reciter Suggestion Targets // ---------------------------------------------------------------------------- @@ -25,12 +19,21 @@ export function reciterSuggestionTargetStateSelector(): MemoizedSelector { + return createSelector(reciterSuggestionTargetStateSelector(),(state: SuggestionTargetState) => state.sources[source]); +} + +/** + * Returns the Reciter Suggestion Targets list by source. * @function reciterSuggestionTargetObjectSelector - * @return {OpenaireReciterSuggestionTarget[]} + * @return {OpenaireSuggestionTarget[]} */ -export function reciterSuggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); +export function reciterSuggestionTargetObjectSelector(source: string): MemoizedSelector { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state.targets); } /** @@ -38,60 +41,60 @@ export function reciterSuggestionTargetObjectSelector(): MemoizedSelector state.suggestionTarget.loaded -); +export const isReciterSuggestionTargetLoadedSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.loaded || false); +}; /** * Returns true if the deduplication sets are processing. * @function isDeduplicationSetsProcessingSelector * @return {boolean} */ -export const isreciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.processing -); +export const isreciterSuggestionTargetProcessingSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.processing || false); +}; /** * Returns the total available pages of Reciter Suggestion Targets. * @function getreciterSuggestionTargetTotalPagesSelector * @return {number} */ -export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.totalPages -); +export const getReciterSuggestionTargetTotalPagesSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalPages || 0); +}; /** * Returns the current page of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetCurrentPageSelector + * @function getReciterSuggestionTargetCurrentPageSelector * @return {number} */ -export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentPage -); +export const getReciterSuggestionTargetCurrentPageSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.currentPage || 0); +}; /** * Returns the total number of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetTotalsSelector + * @function getReciterSuggestionTargetTotalsSelector * @return {number} */ -export const getreciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.totalElements -); +export const getReciterSuggestionTargetTotalsSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalElements || 0); +}; /** * Returns Suggestion Targets for the current user. * @function getCurrentUserReciterSuggestionTargetSelector * @return {OpenaireSuggestionTarget[]} */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentUserTargets -); +export const getCurrentUserSuggestionTargetsSelector = () => { + return createSelector(reciterSuggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargets || []); +}; /** - * Returns whether or not the user has consulted their suggestions + * Returns whether the user has consulted their suggestions * @function getCurrentUserReciterSuggestionTargetSelector * @return {boolean} */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentUserTargetsVisited -); +export const getCurrentUserSuggestionTargetsVisitedSelector = () => { + return createSelector(reciterSuggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargetsVisited || false); +}; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts index 40bc204b7cc..c2d3e800dcb 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts @@ -2,10 +2,12 @@ import { Action } from '@ngrx/store'; import { type } from '../../../shared/ngrx/type'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; /** - * For each action type in an action group, make a simple + * For each action type in A action group, make a simple * enum object for all of this group's action types. * * The 'type' utility function coerces strings into string @@ -23,7 +25,7 @@ export const SuggestionTargetActionTypes = { }; /** - * An ngrx action to retrieve all the Suggestion Targets. + * A ngrx action to retrieve all the Suggestion Targets. */ export class RetrieveTargetsBySourceAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; @@ -53,18 +55,34 @@ export class RetrieveTargetsBySourceAction implements Action { } /** - * An ngrx action for retrieving 'all Suggestion Targets' error. + * A ngrx action for notifying error. */ -export class RetrieveAllTargetsErrorAction implements Action { +export class RetrieveTargetsBySourceErrorAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR; + payload: { + source: string; + }; + + /** + * Create a new RetrieveTargetsBySourceAction. + * + * @param source + * the source for which to retrieve suggestion targets + */ + constructor(source: string) { + this.payload = { + source + }; + } } /** - * An ngrx action to load the Suggestion Target objects. + * A ngrx action to load the Suggestion Target objects. */ export class AddTargetAction implements Action { type = SuggestionTargetActionTypes.ADD_TARGETS; payload: { + source: string; targets: OpenaireSuggestionTarget[]; totalPages: number; currentPage: number; @@ -74,6 +92,8 @@ export class AddTargetAction implements Action { /** * Create a new AddTargetAction. * + * @param source + * the source of suggestion targets * @param targets * the list of targets * @param totalPages @@ -83,8 +103,9 @@ export class AddTargetAction implements Action { * @param totalElements * the total available Suggestion Targets */ - constructor(targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + constructor(source: string, targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { this.payload = { + source, targets, totalPages, currentPage, @@ -95,7 +116,7 @@ export class AddTargetAction implements Action { } /** - * An ngrx action to load the user Suggestion Target object. + * A ngrx action to load the user Suggestion Target object. * Called by the ??? effect. */ export class AddUserSuggestionsAction implements Action { @@ -117,7 +138,7 @@ export class AddUserSuggestionsAction implements Action { } /** - * An ngrx action to reload the user Suggestion Target object. + * A ngrx action to reload the user Suggestion Target object. * Called by the ??? effect. */ export class RefreshUserSuggestionsAction implements Action { @@ -125,7 +146,7 @@ export class RefreshUserSuggestionsAction implements Action { } /** - * An ngrx action to Mark User Suggestions As Visited. + * A ngrx action to Mark User Suggestions As Visited. * Called by the ??? effect. */ export class MarkUserSuggestionsAsVisitedAction implements Action { @@ -133,10 +154,25 @@ export class MarkUserSuggestionsAsVisitedAction implements Action { } /** - * An ngrx action to clear targets state. + * A ngrx action to clear targets state. */ export class ClearSuggestionTargetsAction implements Action { type = SuggestionTargetActionTypes.CLEAR_TARGETS; + payload: { + source: string; + }; + + /** + * Create a new ClearSuggestionTargetsAction. + * + * @param source + * the source of suggestion targets + */ + constructor(source: string) { + this.payload = { + source + }; + } } /** @@ -149,4 +185,4 @@ export type SuggestionTargetsActions | ClearSuggestionTargetsAction | MarkUserSuggestionsAsVisitedAction | RetrieveTargetsBySourceAction - | RetrieveAllTargetsErrorAction; + | RetrieveTargetsBySourceErrorAction; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html index 791e694ba90..d7e669051b0 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html @@ -3,8 +3,8 @@
- - + { this.getSuggestionTargets(); @@ -95,8 +97,8 @@ export class SuggestionTargetsComponent implements OnInit { * @return Observable * 'true' if the targets are loading, 'false' otherwise. */ - public isTargetsLoading(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(); + public isTargetsLoading(source: string): Observable { + return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(source); } /** @@ -106,7 +108,7 @@ export class SuggestionTargetsComponent implements OnInit { * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ public isTargetsProcessing(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(); + return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(this.source); } /** @@ -125,7 +127,7 @@ export class SuggestionTargetsComponent implements OnInit { * Unsubscribe from all subscriptions. */ ngOnDestroy(): void { - this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); + this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.source); this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index fe9311b5412..3f3db5373e2 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -10,8 +10,8 @@ import { AddTargetAction, AddUserSuggestionsAction, RefreshUserSuggestionsAction, - RetrieveAllTargetsErrorAction, RetrieveTargetsBySourceAction, + RetrieveTargetsBySourceErrorAction, SuggestionTargetActionTypes, } from './suggestion-targets.actions'; import { PaginatedList } from '../../../core/data/paginated-list.model'; @@ -41,13 +41,13 @@ export class SuggestionTargetsEffects { action.payload.currentPage ).pipe( map((targets: PaginatedList) => - new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements) + new AddTargetAction(action.payload.source, targets.page, targets.totalPages, targets.currentPage, targets.totalElements) ), catchError((error: Error) => { if (error) { console.error(error.message); } - return of(new RetrieveAllTargetsErrorAction()); + return of(new RetrieveTargetsBySourceErrorAction(action.payload.source)); }) ); }) diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts index f8bd53ec050..0df541974d4 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts @@ -1,16 +1,27 @@ import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; /** * The interface representing the OpenAIRE suggestion targets state. */ -export interface SuggestionTargetState { +export interface SuggestionTargetEntry { targets: OpenaireSuggestionTarget[]; processing: boolean; loaded: boolean; totalPages: number; currentPage: number; totalElements: number; + +} + +export interface SuggestionSourcesState { + [source: string]: SuggestionTargetEntry; +} + +export interface SuggestionTargetState { + sources: SuggestionSourcesState; currentUserTargets: OpenaireSuggestionTarget[]; currentUserTargetsVisited: boolean; } @@ -18,13 +29,17 @@ export interface SuggestionTargetState { /** * Used for the OpenAIRE Suggestion Target state initialization. */ -const SuggestionTargetInitialState: SuggestionTargetState = { +const suggestionSourceTargetsInitialState: SuggestionTargetEntry = { targets: [], processing: false, loaded: false, totalPages: 0, currentPage: 0, totalElements: 0, +}; + +const SuggestionTargetInitialState: SuggestionTargetState = { + sources: {}, currentUserTargets: null, currentUserTargetsVisited: false }; @@ -42,25 +57,42 @@ const SuggestionTargetInitialState: SuggestionTargetState = { export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { switch (action.type) { case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], - processing: true + processing: true, + }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) }); } case SuggestionTargetActionTypes.ADD_TARGETS: { - return Object.assign({}, state, { - targets: state.targets.concat(action.payload.targets), + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { + targets: sourceState.targets.concat(action.payload.targets), processing: false, loaded: true, totalPages: action.payload.totalPages, - currentPage: state.currentPage, + currentPage: action.payload.currentPage, totalElements: action.payload.totalElements }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, loaded: true, @@ -68,6 +100,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { @@ -83,14 +122,22 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a } case SuggestionTargetActionTypes.CLEAR_TARGETS: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, - loaded: false, + loaded: true, totalPages: 0, currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } default: { diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts index 2e05bce0a9b..92cf1dba3b0 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts @@ -7,13 +7,15 @@ import { map } from 'rxjs/operators'; import { getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsVisitedSelector, - getreciterSuggestionTargetCurrentPageSelector, - getreciterSuggestionTargetTotalsSelector, + getReciterSuggestionTargetCurrentPageSelector, + getReciterSuggestionTargetTotalsSelector, isReciterSuggestionTargetLoadedSelector, isreciterSuggestionTargetProcessingSelector, reciterSuggestionTargetObjectSelector } from '../selectors'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; import { ClearSuggestionTargetsAction, MarkUserSuggestionsAsVisitedAction, @@ -40,8 +42,8 @@ export class SuggestionTargetsStateService { * @return Observable * The list of Reciter Suggestion Targets. */ - public getReciterSuggestionTargets(): Observable { - return this.store.pipe(select(reciterSuggestionTargetObjectSelector())); + public getReciterSuggestionTargets(source: string): Observable { + return this.store.pipe(select(reciterSuggestionTargetObjectSelector(source))); } /** @@ -50,9 +52,9 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loading, 'false' otherwise. */ - public isReciterSuggestionTargetsLoading(): Observable { + public isReciterSuggestionTargetsLoading(source: string): Observable { return this.store.pipe( - select(isReciterSuggestionTargetLoadedSelector), + select(isReciterSuggestionTargetLoadedSelector(source)), map((loaded: boolean) => !loaded) ); } @@ -63,8 +65,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loaded, 'false' otherwise. */ - public isReciterSuggestionTargetsLoaded(): Observable { - return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector)); + public isReciterSuggestionTargetsLoaded(source: string): Observable { + return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector(source))); } /** @@ -73,8 +75,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ - public isReciterSuggestionTargetsProcessing(): Observable { - return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector)); + public isReciterSuggestionTargetsProcessing(source: string): Observable { + return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector(source))); } /** @@ -83,8 +85,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Reciter Suggestion Targets pages. */ - public getReciterSuggestionTargetsTotalPages(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + public getReciterSuggestionTargetsTotalPages(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector(source))); } /** @@ -93,8 +95,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the current Reciter Suggestion Targets page. */ - public getReciterSuggestionTargetsCurrentPage(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetCurrentPageSelector)); + public getReciterSuggestionTargetsCurrentPage(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetCurrentPageSelector(source))); } /** @@ -103,8 +105,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Reciter Suggestion Targets. */ - public getReciterSuggestionTargetsTotals(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + public getReciterSuggestionTargetsTotals(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector(source))); } /** @@ -128,7 +130,7 @@ export class SuggestionTargetsStateService { * The Reciter Suggestion Targets object. */ public getCurrentUserSuggestionTargets(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector())); } /** @@ -138,7 +140,7 @@ export class SuggestionTargetsStateService { * True if user already visited, false otherwise. */ public hasUserVisitedSuggestions(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector())); } /** @@ -150,9 +152,12 @@ export class SuggestionTargetsStateService { /** * Dispatch an action to clear the Reciter Suggestion Targets state. + * + * @param source + * the source of suggestion targets */ - public dispatchClearSuggestionTargetsAction(): void { - this.store.dispatch(new ClearSuggestionTargetsAction()); + public dispatchClearSuggestionTargetsAction(source: string): void { + this.store.dispatch(new ClearSuggestionTargetsAction(source)); } /** diff --git a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts index 094dfab0174..923c25ecf41 100644 --- a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts +++ b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts @@ -29,6 +29,7 @@ export class SuggestionsNotificationComponent implements OnInit { ngOnInit() { this.suggestionsRD$ = this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(); + this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } /** diff --git a/src/app/root.module.ts b/src/app/root.module.ts index a4edec2a914..61069afe4c9 100644 --- a/src/app/root.module.ts +++ b/src/app/root.module.ts @@ -42,10 +42,7 @@ import { import { FooterModule } from './footer/footer.module'; import { SocialModule } from './social/social.module'; import { ExploreModule } from './shared/explore/explore.module'; -import { BreadcrumbTooltipPipe } from './breadcrumbs/breadcrumb/breadcrumb-tooltip.pipe'; -import { - TruncateBreadcrumbItemCharactersPipe -} from './breadcrumbs/breadcrumb/truncate-breadcrumb-item-characters.pipe'; +import { OpenaireModule } from './openaire/openaire.module'; const IMPORTS = [ CommonModule, @@ -55,7 +52,8 @@ const IMPORTS = [ NgbModule, ExploreModule, FooterModule, - SocialModule + SocialModule, + OpenaireModule ]; const PROVIDERS = [ @@ -87,8 +85,6 @@ const DECLARATIONS = [ ThemedPageErrorComponent, PageErrorComponent, ContextHelpToggleComponent, - TruncateBreadcrumbItemCharactersPipe, - BreadcrumbTooltipPipe ]; const EXPORTS = [ diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 7862339dc35..7c8f76e6487 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -1,7 +1,7 @@ import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SearchComponent } from '../shared/search/search.component'; -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, PLATFORM_ID } from '@angular/core'; import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; @@ -32,9 +32,10 @@ export class ConfigurationSearchPageComponent extends SearchComponent { protected searchManager: SearchManager, protected sidebarService: SidebarService, protected windowService: HostWindowService, + @Inject(PLATFORM_ID) public platformId: any, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, protected routeService: RouteService, protected router: Router) { - super(service, searchManager, sidebarService, windowService, searchConfigService, routeService, router); + super(service, searchManager, sidebarService, windowService, searchConfigService, platformId, routeService, router); } } diff --git a/src/app/search-page/search-page.component.html b/src/app/search-page/search-page.component.html index ae9beb80d3f..86ed2bff50d 100644 --- a/src/app/search-page/search-page.component.html +++ b/src/app/search-page/search-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/browse-most-elements/browse-most-elements.component.ts b/src/app/shared/browse-most-elements/browse-most-elements.component.ts index 63af28ad53c..ed616957ad6 100644 --- a/src/app/shared/browse-most-elements/browse-most-elements.component.ts +++ b/src/app/shared/browse-most-elements/browse-most-elements.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectorRef, Component, Inject, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; import { SearchService } from '../../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; @@ -37,12 +38,17 @@ export class BrowseMostElementsComponent implements OnInit { constructor( @Inject(APP_CONFIG) protected appConfig: AppConfig, + @Inject(PLATFORM_ID) private platformId: Object, private searchService: SearchService, private cdr: ChangeDetectorRef) { } ngOnInit() { + if (isPlatformServer(this.platformId)) { + return; + } + const showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails; const followLinks = showThumbnails ? [followLink('thumbnail')] : []; this.searchService.search(this.paginatedSearchOptions, null, true, true, ...followLinks).pipe( diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts index db50de91833..3d7860d5e38 100644 --- a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts @@ -1,4 +1,5 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -39,11 +40,16 @@ export class CountersSectionComponent implements OnInit { constructor(private searchService: SearchService, private uuidService: UUIDService, - @Inject(NativeWindowService) protected _window: NativeWindowRef) { + @Inject(PLATFORM_ID) private platformId: Object, + @Inject(NativeWindowService) protected _window: NativeWindowRef,) { } ngOnInit() { + if (isPlatformServer(this.platformId)) { + return; + } + this.counterData$ = forkJoin( this.countersSection.counterSettingsList.map((counterSettings: CountersSettings) => this.searchService.search(new PaginatedSearchOptions({ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts index ca37e1bd38f..4ffb8ec03a6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts @@ -291,9 +291,16 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp private buildChipItem() { const item = Object.create({}); + let mainModel; + this.formModel.some((modelRow: DynamicFormGroupModel) => { + const findIndex = modelRow.group.findIndex(model => model.name === this.model.name); + if (findIndex !== -1) { + mainModel = modelRow.group[findIndex]; + return true; + } + }); this.formModel.forEach((row) => { const modelRow = row as DynamicFormGroupModel; - const mainRow: any = modelRow.group.find(model => model.name === this.model.name); modelRow.group.forEach((control: DynamicInputModel) => { const controlValue: any = (control?.value as any)?.value || control?.value || PLACEHOLDER_PARENT_METADATA; const controlAuthority: any = (control?.value as any)?.authority || null; @@ -301,7 +308,7 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp item[control.name] = new FormFieldMetadataValueObject( controlValue, (control as any)?.language, - controlValue === PLACEHOLDER_PARENT_METADATA ? null : mainRow.securityLevel, + controlValue === PLACEHOLDER_PARENT_METADATA ? null : mainModel.securityLevel, controlAuthority, null, 0, null, (control?.value as any)?.otherInformation || null diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts index 29f0e92012f..2ea9ab0fb71 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts @@ -58,6 +58,7 @@ export class RelationGroupFieldParser extends FieldParser { } }; + this.initSecurityValue(modelConfiguration); const model = new DynamicRelationGroupModel(modelConfiguration, cls); model.name = this.getFieldId(); model.isInlineGroup = (this.configData.input.type === ParserType.InlineGroup); 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 48665256ce7..6aadd2a7c71 100644 --- a/src/app/shared/metric/metric-loader/base-metric.component.scss +++ b/src/app/shared/metric/metric-loader/base-metric.component.scss @@ -9,7 +9,7 @@ .container.alert.metric-container { max-height: inherit; min-height: 7.1em; - height: 90%; + height: 100%; .btn-overlap-container { display: none !important; diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index e8e3899dac2..26c9d68e0ab 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -6,7 +6,8 @@ import { Input, OnDestroy, OnInit, - Output + Output, + PLATFORM_ID } from '@angular/core'; import { NavigationStart, Router } from '@angular/router'; @@ -49,6 +50,7 @@ import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-ro import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; import { SearchManager } from '../../core/browse/search-manager'; import { AlertType } from '../alert/alert-type'; +import { isPlatformServer } from '@angular/common'; @Component({ selector: 'ds-search', @@ -222,6 +224,11 @@ export class SearchComponent implements OnInit, OnDestroy { */ @Input() showFilterToggle = false; + /** + * Defines whether to show the toggle button to Show/Hide filter + */ + @Input() renderOnServerSide = true; + /** * Defines whether to show the toggle button to Show/Hide chart */ @@ -358,9 +365,10 @@ export class SearchComponent implements OnInit, OnDestroy { protected searchManager: SearchManager, protected sidebarService: SidebarService, protected windowService: HostWindowService, + @Inject(PLATFORM_ID) public platformId: any, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, protected routeService: RouteService, - protected router: Router) { + protected router: Router,) { this.isXsOrSm$ = this.windowService.isXsOrSm(); } @@ -372,6 +380,11 @@ export class SearchComponent implements OnInit, OnDestroy { * If something changes, update the list of scopes for the dropdown */ ngOnInit(): void { + if (!this.renderOnServerSide && isPlatformServer(this.platformId)) { + this.initialized$.next(true); + return; + } + if (this.useUniquePageId) { // Create an unique pagination id related to the instance of the SearchComponent this.paginationId = uniqueId(this.paginationId); diff --git a/src/app/shared/search/themed-search.component.ts b/src/app/shared/search/themed-search.component.ts index 944412b1005..710bcc1071a 100644 --- a/src/app/shared/search/themed-search.component.ts +++ b/src/app/shared/search/themed-search.component.ts @@ -20,7 +20,7 @@ import { AlertType } from '../alert/alert-type'; templateUrl: '../theme-support/themed.component.html', }) export class ThemedSearchComponent extends ThemedComponent { - protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'forcedEmbeddedKeys', 'useCachedVersionIfAvailable', 'collapseCharts', 'collapseFilters', 'inPlaceSearch', 'linkType', 'paginationId', 'projection', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCharts', 'showExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'showFilterToggle', 'showChartsToggle', 'showCsvExport', 'resultFound', 'deselectObject', 'selectObject', 'customEvent', 'trackStatistics', 'query', 'searchResultNotice', 'searchResultNoticeType', 'showSearchResultNotice']; + protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'forcedEmbeddedKeys', 'useCachedVersionIfAvailable', 'collapseCharts', 'collapseFilters', 'inPlaceSearch', 'linkType', 'paginationId', 'projection', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCharts', 'showExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'showFilterToggle', 'showChartsToggle', 'showCsvExport', 'resultFound', 'deselectObject', 'selectObject', 'customEvent', 'trackStatistics', 'query', 'searchResultNotice', 'searchResultNoticeType', 'showSearchResultNotice', 'renderOnServerSide']; @Input() configurationList: SearchConfigurationOption[]; @@ -88,6 +88,8 @@ export class ThemedSearchComponent extends ThemedComponent { @Input() query: string; + @Input() renderOnServerSide = false; + @Output() resultFound: EventEmitter> = new EventEmitter(); @Output() deselectObject: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html index 287175bfa10..d310bc2a4f5 100644 --- a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html +++ b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html @@ -17,9 +17,9 @@
- + + [for]="'checkbox-' + subscriptionType.key + frequency">{{ 'subscriptions.modal.new-subscription-form.frequency.' + frequency | translate }}
diff --git a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.ts b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.ts index 55a3f8d707f..f4d8df835ac 100644 --- a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.ts +++ b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.ts @@ -228,7 +228,6 @@ export class SubscriptionModalComponent implements OnInit { }), toArray(), tap((res: RemoteData[]) => { - console.log(res); const successTypes = res.filter((rd: RemoteData) => rd.hasSucceeded) .map((rd: RemoteData) => rd.payload.subscriptionType); const failedTypes = res.filter((rd: RemoteData) => rd.hasFailed); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 1d076b86435..ed99e6929d0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2105,7 +2105,7 @@ "error.validation.custom-url.invalid-characters": "The custom url contains invalid characters.", - "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", + "error.validation.license.required": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", @@ -2137,6 +2137,8 @@ "error.validation.notRepeatable": "This field is not repeatable, please choose only one value and discard the others.", + "error.validation.detect-duplicate": "The handling of the detected duplicates is mandatory", + "event.listelement.badge": "Event", 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..c4a65f1bb3e 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -737,6 +737,4 @@ export class DefaultAppConfig implements AppConfig { additionalMetadataFields: [], authorMetadata: ['dc.contributor.author', 'dc.creator', 'dc.contributor.*'], }; - - breadcrumbCharLimit = 10; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 6956db23dcb..7e5f5c280f6 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -553,6 +553,4 @@ export const environment: BuildConfig = { ], authorMetadata: ['dc.contributor.author', 'dc.contributor.editor', 'dc.contributor.contributor', 'dc.creator'], }, - - breadcrumbCharLimit: 10, };