diff --git a/config/config.example.yml b/config/config.example.yml index c548d6944a..af04859201 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -121,6 +121,9 @@ languages: - code: en label: English active: true + - code: ca + label: Català + active: true - code: cs label: Čeština active: true diff --git a/package.json b/package.json index f6ab1274e6..dcb629a331 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "deepmerge": "^4.2.2", + "ejs": "^3.1.8", "express": "^4.17.1", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.0.0-1", @@ -147,6 +148,7 @@ "@ngtools/webpack": "^13.2.6", "@nguniversal/builders": "^13.1.1", "@types/deep-freeze": "0.1.2", + "@types/ejs": "^3.1.1", "@types/express": "^4.17.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", diff --git a/server.ts b/server.ts index 608c214076..ecbbb982d4 100644 --- a/server.ts +++ b/server.ts @@ -22,6 +22,7 @@ import 'rxjs'; /* eslint-disable import/no-namespace */ import * as morgan from 'morgan'; import * as express from 'express'; +import * as ejs from 'ejs'; import * as compression from 'compression'; import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ @@ -136,10 +137,23 @@ export function app() { })(_, (options as any), callback) ); + server.engine('ejs', ejs.renderFile); + /* * Register the view engines for html and ejs */ server.set('view engine', 'html'); + server.set('view engine', 'ejs'); + + /** + * Serve the robots.txt ejs template, filling in the origin variable + */ + server.get('/robots.txt', (req, res) => { + res.setHeader('content-type', 'text/plain'); + res.render('assets/robots.txt.ejs', { + 'origin': req.protocol + '://' + req.headers.host + }); + }); /* * Set views folder path to directory where template files are stored diff --git a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts index c4a67349a5..ffa7e882af 100644 --- a/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts +++ b/src/app/browse-by/browse-by-date-page/browse-by-date-page.component.ts @@ -65,6 +65,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent { const searchOptions = browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails); this.updatePageWithItems(searchOptions, this.value, undefined); this.updateParent(params.scope); + this.updateLogo(); this.updateStartsWithOptions(this.browseId, metadataKeys, params.scope); })); } diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html index 227fa8aa78..b68f498771 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.html @@ -5,6 +5,11 @@ + + + { route.params = observableOf(paramsWithValue); comp.ngOnInit(); + comp.updateParent('fake-scope'); + comp.updateLogo(); + fixture.detectChanges(); }); it('should fetch items', () => { @@ -151,6 +154,10 @@ describe('BrowseByMetadataPageComponent', () => { expect(result.payload.page).toEqual(mockItems); }); }); + + it('should fetch the logo', () => { + expect(comp.logo$).toBeTruthy(); + }); }); describe('when calling browseParamsToOptions', () => { diff --git a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts index 4cfe332da1..5de6c7d856 100644 --- a/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts +++ b/src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.ts @@ -15,7 +15,11 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { StartsWithType } from '../../shared/starts-with/starts-with-decorator'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { map } from 'rxjs/operators'; +import { filter, map, mergeMap } from 'rxjs/operators'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { Collection } from '../../core/shared/collection.model'; +import { Community } from '../../core/shared/community.model'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; export const BBM_PAGINATION_ID = 'bbm'; @@ -48,6 +52,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { */ parent$: Observable>; + /** + * The logo of the current Community or Collection + */ + logo$: Observable>; + /** * The pagination config used to display the values */ @@ -151,6 +160,7 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { this.updatePage(browseParamsToOptions(params, currentPage, currentSort, this.browseId, false)); } this.updateParent(params.scope); + this.updateLogo(); })); this.updateStartsWithTextOptions(); @@ -196,12 +206,31 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy { */ updateParent(scope: string) { if (hasValue(scope)) { - this.parent$ = this.dsoService.findById(scope).pipe( + const linksToFollow = () => { + return [followLink('logo')]; + }; + this.parent$ = this.dsoService.findById(scope, + true, + true, + ...linksToFollow() as FollowLinkConfig[]).pipe( getFirstSucceededRemoteData() ); } } + /** + * Update the parent Community or Collection logo + */ + updateLogo() { + if (hasValue(this.parent$)) { + this.logo$ = this.parent$.pipe( + map((rd: RemoteData) => rd.payload), + filter((collectionOrCommunity: Collection | Community) => hasValue(collectionOrCommunity.logo)), + mergeMap((collectionOrCommunity: Collection | Community) => collectionOrCommunity.logo) + ); + } + } + /** * Navigate to the previous page */ diff --git a/src/app/browse-by/browse-by-page.module.ts b/src/app/browse-by/browse-by-page.module.ts index fea6668b3c..554a6c4f46 100644 --- a/src/app/browse-by/browse-by-page.module.ts +++ b/src/app/browse-by/browse-by-page.module.ts @@ -4,16 +4,21 @@ import { BrowseByModule } from './browse-by.module'; import { ItemDataService } from '../core/data/item-data.service'; import { BrowseService } from '../core/browse/browse.service'; import { BrowseByGuard } from './browse-by-guard'; +import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module'; @NgModule({ imports: [ + SharedBrowseByModule, BrowseByRoutingModule, - BrowseByModule.withEntryComponents() + BrowseByModule.withEntryComponents(), ], providers: [ ItemDataService, BrowseService, - BrowseByGuard + BrowseByGuard, + ], + declarations: [ + ] }) export class BrowseByPageModule { diff --git a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts index 5320d7bb48..3e9af2197c 100644 --- a/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/browse-by/browse-by-title-page/browse-by-title-page.component.ts @@ -49,6 +49,7 @@ export class BrowseByTitlePageComponent extends BrowseByMetadataPageComponent { this.browseId = params.id || this.defaultBrowseId; this.updatePageWithItems(browseParamsToOptions(params, currentPage, currentSort, this.browseId, this.fetchThumbnails), undefined, undefined); this.updateParent(params.scope); + this.updateLogo(); })); this.updateStartsWithTextOptions(); } diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index 14e21f8b4c..a7643464dc 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { BrowseByTitlePageComponent } from './browse-by-title-page/browse-by-title-page.component'; -import { SharedModule } from '../shared/shared.module'; import { BrowseByMetadataPageComponent } from './browse-by-metadata-page/browse-by-metadata-page.component'; import { BrowseByDatePageComponent } from './browse-by-date-page/browse-by-date-page.component'; import { BrowseBySwitcherComponent } from './browse-by-switcher/browse-by-switcher.component'; @@ -10,6 +9,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module'; import { ThemedBrowseByMetadataPageComponent } from './browse-by-metadata-page/themed-browse-by-metadata-page.component'; import { ThemedBrowseByDatePageComponent } from './browse-by-date-page/themed-browse-by-date-page.component'; import { ThemedBrowseByTitlePageComponent } from './browse-by-title-page/themed-browse-by-title-page.component'; +import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -25,9 +25,9 @@ const ENTRY_COMPONENTS = [ @NgModule({ imports: [ + SharedBrowseByModule, CommonModule, ComcolModule, - SharedModule ], declarations: [ BrowseBySwitcherComponent, @@ -45,7 +45,7 @@ export class BrowseByModule { */ static withEntryComponents() { return { - ngModule: SharedModule, + ngModule: SharedBrowseByModule, providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) }; } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 66d9b9921c..553b437d71 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,7 +8,12 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { ItemMock, MockBitstream1, MockBitstream3 } from '../../shared/mocks/item.mock'; +import { + ItemMock, + MockBitstream1, + MockBitstream3, + MockBitstream2 +} from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -24,6 +29,7 @@ import { HardRedirectService } from '../services/hard-redirect.service'; import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { AppConfig } from '../../../config/app-config.interface'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -44,6 +50,8 @@ describe('MetadataService', () => { let router: Router; let store; + let appConfig: AppConfig; + const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; @@ -86,6 +94,14 @@ describe('MetadataService', () => { store = getMockStore({ initialState }); spyOn(store, 'dispatch'); + appConfig = { + item: { + bitstream: { + pageSize: 5 + } + } + } as any; + metadataService = new MetadataService( router, translateService, @@ -98,6 +114,7 @@ describe('MetadataService', () => { rootService, store, hardRedirectService, + appConfig, authorizationService ); }); @@ -358,29 +375,66 @@ describe('MetadataService', () => { }); })); - it('should link to first Bitstream with allowed format', fakeAsync(() => { - const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1]; - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); - (bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues( - ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), - ); + describe(`when there's a bitstream with an allowed format on the first page`, () => { + let bitstreams; - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + beforeEach(() => { + bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1]; + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); + (bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues( + ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), + ); + }); + + it('should link to first Bitstream with allowed format', fakeAsync(() => { + (metadataService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + } } - } - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download' - }); - })); + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download' + }); + })); + + }); + }); }); + describe(`when there's no bitstream with an allowed format on the first page`, () => { + let bitstreams; + + beforeEach(() => { + bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2]; + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); + (bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues( + ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), + ); + }); + + it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => { + (metadataService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + } + } + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download' + }); + })); + + }); + + describe('tagstore', () => { beforeEach(fakeAsync(() => { (metadataService as any).processRouteChange({ diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 9d5ee80bba..204c925e6b 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,14 +1,21 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, combineLatest, EMPTY, Observable, of as observableOf } from 'rxjs'; -import { expand, filter, map, switchMap, take } from 'rxjs/operators'; +import { + BehaviorSubject, + combineLatest, + Observable, + of as observableOf, + concat as observableConcat, + EMPTY +} from 'rxjs'; +import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; @@ -37,6 +44,7 @@ import { coreSelector } from '../core.selectors'; import { CoreState } from '../core-state.model'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { getDownloadableBitstream } from '../shared/bitstream.operators'; +import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; /** * The base selector function to select the metaTag section in the store @@ -87,6 +95,7 @@ export class MetadataService { private rootService: RootDataService, private store: Store, private hardRedirectService: HardRedirectService, + @Inject(APP_CONFIG) private appConfig: AppConfig, private authorizationService: AuthorizationDataService ) { } @@ -298,7 +307,13 @@ export class MetadataService { true, true, followLink('primaryBitstream'), - followLink('bitstreams', {}, followLink('format')), + followLink('bitstreams', { + findListOptions: { + // limit the number of bitstreams used to find the citation pdf url to the number + // shown by default on an item page + elementsPerPage: this.appConfig.item.bitstream.pageSize + } + }, followLink('format')), ).pipe( getFirstSucceededRemoteDataPayload(), switchMap((bundle: Bundle) => @@ -363,64 +378,45 @@ export class MetadataService { } /** - * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type + * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream + * with a MIME type. + * + * Note this will only check the current page (page size determined item.bitstream.pageSize in the + * config) of bitstreams for performance reasons. + * See https://github.com/DSpace/DSpace/issues/8648 for more info + * * included in {@linkcode CITATION_PDF_URL_MIMETYPES} * @param bitstreamRd * @private */ private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData>): Observable { - return observableOf(bitstreamRd.payload).pipe( - // Because there can be more than one page of bitstreams, this expand operator - // will retrieve them in turn. Due to the take(1) at the bottom, it will only - // retrieve pages until a match is found - expand((paginatedList: PaginatedList) => { - if (hasNoValue(paginatedList.next)) { - // If there's no next page, stop. - return EMPTY; - } else { - // Otherwise retrieve the next page - return this.bitstreamDataService.findListByHref( - paginatedList.next, - undefined, - true, - true, - followLink('format') - ).pipe( - getFirstCompletedRemoteData(), - map((next: RemoteData>) => { - if (hasValue(next.payload)) { - return next.payload; - } else { - return EMPTY; - } - }) - ); - } - }), - // Return the array of bitstreams inside each paginated list - map((paginatedList: PaginatedList) => paginatedList.page), - // Emit the bitstreams in the list one at a time - switchMap((bitstreams: Bitstream[]) => bitstreams), - // Retrieve the format for each bitstream - switchMap((bitstream: Bitstream) => bitstream.format.pipe( - getFirstSucceededRemoteDataPayload(), - // Keep the original bitstream, because it, not the format, is what we'll need - // for the link at the end - map((format: BitstreamFormat) => [bitstream, format]) - )), - // Check if bitstream downloadable - switchMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe( - getDownloadableBitstream(this.authorizationService), - map((bit: Bitstream) => [bit, format]) - )), - // Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check - filter(([bitstream, format]: [Bitstream, BitstreamFormat]) => - hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), - // We only need 1 - take(1), - // Emit the link of the match - map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)) - ); + if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) { + // Retrieve the formats of all bitstreams in the page sequentially + return observableConcat( + ...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe( + getFirstSucceededRemoteDataPayload(), + // Keep the original bitstream, because it, not the format, is what we'll need + // for the link at the end + map((format: BitstreamFormat) => [bitstream, format]) + )) + ).pipe( + // Verify that the bitstream is downloadable + mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe( + getDownloadableBitstream(this.authorizationService), + map((bit: Bitstream) => [bit, format]) + )), + // Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check + filter(([bitstream, format]: [Bitstream, BitstreamFormat]) => + hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), + // We only need 1 + take(1), + // Emit the link of the match + // tap((v) => console.log('result', v)), + map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)) + ); + } else { + return EMPTY; + } } /** diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index bf73a62447..2d1f9b477e 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss index 3575cae797..b810d2ddd4 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss @@ -1 +1,2 @@ @import '../../../../../styles/variables.scss'; + diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index b370431a27..010b5ee5b5 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index 27ee373237..88ed9e17dd 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 4469a2ac29..7e20edca6b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -34,6 +34,7 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history import { VersionDataService } from '../../../../core/data/version-data.service'; import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; import { SearchService } from '../../../../core/shared/search/search.service'; +import { mockRouteService } from '../../../../item-page/simple/item-types/shared/item.component.spec'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -99,7 +100,7 @@ describe('JournalComponent', () => { { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, - { provide: RouteService, useValue: {} } + { provide: RouteService, useValue: mockRouteService } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index 3bf861e10d..07585d4d16 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -20,6 +20,7 @@ import { JournalVolumeSidebarSearchListElementComponent } from './item-list-elem import { JournalIssueSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component'; import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component'; import { ItemSharedModule } from '../../item-page/item-shared.module'; +import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -47,7 +48,8 @@ const ENTRY_COMPONENTS = [ imports: [ CommonModule, ItemSharedModule, - SharedModule + SharedModule, + ResultsBackButtonModule ], declarations: [ ...ENTRY_COMPONENTS diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index ac92e4ad6d..d8b3815f6e 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index a73236006b..20e458a906 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 3fb5e2d520..ee75abaae8 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 721a22be08..e3525cc881 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -29,6 +29,7 @@ import { OrgUnitSidebarSearchListElementComponent } from './item-list-elements/s import { PersonSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component'; import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component'; import { ItemSharedModule } from '../../item-page/item-shared.module'; +import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -69,7 +70,8 @@ const COMPONENTS = [ CommonModule, ItemSharedModule, SharedModule, - NgbTooltipModule + NgbTooltipModule, + ResultsBackButtonModule ], declarations: [ ...COMPONENTS, diff --git a/src/app/item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/item-page/edit-item-page/item-operation/item-operation.component.html index ecbc19aea8..85c6a2cca1 100644 --- a/src/app/item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/item-page/edit-item-page/item-operation/item-operation.component.html @@ -5,12 +5,12 @@
- - diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 39c2580921..53c679e596 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -47,6 +47,7 @@ import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component'; import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component'; import { UploadModule } from '../shared/upload/upload.module'; +import { ResultsBackButtonModule } from '../shared/results-back-button/results-back-button.module'; import { ItemAlertsComponent } from './alerts/item-alerts.component'; import { ItemVersionsModule } from './versions/item-versions.module'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; @@ -107,7 +108,8 @@ const DECLARATIONS = [ ResearchEntitiesModule.withEntryComponents(), NgxGalleryModule, NgbAccordionModule, - UploadModule, + ResultsBackButtonModule, + UploadModule ], declarations: [ ...DECLARATIONS, diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 181480b789..9b7861c73c 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -1,3 +1,4 @@ +
{ describe('with IIIF viewer and search', () => { + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/search?query=test%20query&fakeParam=true'); + } + }; beforeEach(waitForAsync(() => { - const localMockRouteService = { - getPreviousUrl(): Observable { - return of('/search?query=test%20query&fakeParam=true'); - } - }; const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [getIIIFEnabled(true)], 'iiif.search.enabled': [getIIIFSearchEnabled(true)], @@ -193,13 +193,12 @@ describe('PublicationComponent', () => { }); describe('with IIIF viewer and search but no previous search query', () => { - + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/item'); + } + }; beforeEach(waitForAsync(() => { - const localMockRouteService = { - getPreviousUrl(): Observable { - return of('/item'); - } - }; const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [getIIIFEnabled(true)], 'iiif.search.enabled': [getIIIFSearchEnabled(true)], diff --git a/src/app/item-page/simple/item-types/shared/item.component.spec.ts b/src/app/item-page/simple/item-types/shared/item.component.spec.ts index c479f04809..cb91a31b06 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.spec.ts @@ -39,6 +39,11 @@ import { RouterTestingModule } from '@angular/router/testing'; import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { ResearcherProfileDataService } from '../../../../core/profile/researcher-profile-data.service'; +import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { Router } from '@angular/router'; +import { ItemComponent } from './item.component'; + export function getIIIFSearchEnabled(enabled: boolean): MetadataValue { return Object.assign(new MetadataValue(), { 'value': enabled, @@ -59,7 +64,11 @@ export function getIIIFEnabled(enabled: boolean): MetadataValue { }); } -export const mockRouteService = jasmine.createSpyObj('RouteService', ['getPreviousUrl']); +export const mockRouteService = { + getPreviousUrl(): Observable { + return observableOf(''); + } +}; /** * Create a generic test for an item-page-fields component using a mockItem and the type of component @@ -114,7 +123,7 @@ export function getItemPageFieldsTest(mockItem: Item, component) { { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, - { provide: RouteService, useValue: {} }, + { provide: RouteService, useValue: mockRouteService }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ResearcherProfileDataService, useValue: {} } ], @@ -376,4 +385,110 @@ describe('ItemComponent', () => { }); }); + const mockItem: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + metadata: { + 'publicationissue.issueNumber': [ + { + language: 'en_US', + value: '1234' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'desc' + } + ] + }, + }); + describe('back to results', () => { + let comp: ItemComponent; + let fixture: ComponentFixture; + let router: Router; + + const searchUrl = '/search?query=test&spc.page=2'; + const browseUrl = '/browse/title?scope=0cc&bbm.page=3'; + const recentSubmissionsUrl = '/collections/be7b8430-77a5-4016-91c9-90863e50583a?cp.page=3'; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule, + ], + declarations: [ItemComponent, GenericItemPageFieldComponent, TruncatePipe ], + providers: [ + { provide: ItemDataService, useValue: {} }, + { provide: TruncatableService, useValue: {} }, + { provide: RelationshipDataService, useValue: {} }, + { provide: ObjectCacheService, useValue: {} }, + { provide: UUIDService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: CommunityDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: VersionHistoryDataService, useValue: {} }, + { provide: VersionDataService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: BitstreamDataService, useValue: {} }, + { provide: WorkspaceitemDataService, useValue: {} }, + { provide: SearchService, useValue: {} }, + { provide: RouteService, useValue: mockRouteService }, + { provide: AuthorizationDataService, useValue: {} }, + { provide: ResearcherProfileDataService, useValue: {} } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }); + })); + + beforeEach(waitForAsync(() => { + router = TestBed.inject(Router); + spyOn(router, 'navigateByUrl'); + TestBed.compileComponents(); + fixture = TestBed.createComponent(ItemComponent); + comp = fixture.componentInstance; + comp.object = mockItem; + fixture.detectChanges(); + })); + + it('should hide back button',() => { + spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf('/item')); + comp.showBackButton.subscribe((val) => { + expect(val).toBeFalse(); + }); + }); + it('should show back button for search', () => { + spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(searchUrl)); + comp.ngOnInit(); + comp.showBackButton.subscribe((val) => { + expect(val).toBeTrue(); + }); + }); + it('should show back button for browse', () => { + spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(browseUrl)); + comp.ngOnInit(); + comp.showBackButton.subscribe((val) => { + expect(val).toBeTrue(); + }); + }); + it('should show back button for recent submissions', () => { + spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(recentSubmissionsUrl)); + comp.ngOnInit(); + comp.showBackButton.subscribe((val) => { + expect(val).toBeTrue(); + }); + }); + }); + }); diff --git a/src/app/item-page/simple/item-types/shared/item.component.ts b/src/app/item-page/simple/item-types/shared/item.component.ts index 71b0b5b678..93e6a0b346 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.ts @@ -5,6 +5,8 @@ import { getItemPageRoute } from '../../../item-page-routing-paths'; import { RouteService } from '../../../../core/services/route.service'; import { Observable } from 'rxjs'; import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif-utils'; +import { filter, map, take } from 'rxjs/operators'; +import { Router } from '@angular/router'; @Component({ selector: 'ds-item', @@ -16,6 +18,17 @@ import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif- export class ItemComponent implements OnInit { @Input() object: Item; + /** + * This regex matches previous routes. The button is shown + * for matching paths and hidden in other cases. + */ + previousRoute = /^(\/search|\/browse|\/collections|\/admin\/search|\/mydspace)/; + + /** + * Used to show or hide the back to results button in the view. + */ + showBackButton: Observable; + /** * Route to the item page */ @@ -38,12 +51,33 @@ export class ItemComponent implements OnInit { mediaViewer; - constructor(protected routeService: RouteService) { + constructor(protected routeService: RouteService, + protected router: Router) { this.mediaViewer = environment.mediaViewer; } + /** + * The function used to return to list from the item. + */ + back = () => { + this.routeService.getPreviousUrl().pipe( + take(1) + ).subscribe( + (url => { + this.router.navigateByUrl(url); + }) + ); + }; + ngOnInit(): void { + this.itemPageRoute = getItemPageRoute(this.object); + // hide/show the back button + this.showBackButton = this.routeService.getPreviousUrl().pipe( + filter(url => this.previousRoute.test(url)), + take(1), + map(() => true) + ); // check to see if iiif viewer is required. this.iiifEnabled = isIiifEnabled(this.object); this.iiifSearchEnabled = isIiifSearchEnabled(this.object); diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index 5c3e5e77b0..fcbc46422d 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -1,3 +1,4 @@ +
+
diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss index 3575cae797..b810d2ddd4 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss @@ -1 +1,2 @@ @import '../../../../../styles/variables.scss'; + diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index 176a692f4b..3581694a5e 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -169,13 +169,12 @@ describe('UntypedItemComponent', () => { }); describe('with IIIF viewer and search', () => { - + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/search?query=test%20query&fakeParam=true'); + } + }; beforeEach(waitForAsync(() => { - const localMockRouteService = { - getPreviousUrl(): Observable { - return of('/search?query=test%20query&fakeParam=true'); - } - }; const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [getIIIFEnabled(true)], 'iiif.search.enabled': [getIIIFSearchEnabled(true)], @@ -183,6 +182,7 @@ describe('UntypedItemComponent', () => { TestBed.overrideProvider(RouteService, {useValue: localMockRouteService}); TestBed.compileComponents(); fixture = TestBed.createComponent(UntypedItemComponent); + spyOn(localMockRouteService, 'getPreviousUrl').and.callThrough(); comp = fixture.componentInstance; comp.object = getItem(iiifEnabledMap); fixture.detectChanges(); @@ -196,17 +196,16 @@ describe('UntypedItemComponent', () => { it('should retrieve the query term for previous route', (): void => { expect(comp.iiifQuery$.subscribe(result => expect(result).toEqual('test query'))); }); - }); describe('with IIIF viewer and search but no previous search query', () => { + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/item'); + } + }; beforeEach(waitForAsync(() => { - const localMockRouteService = { - getPreviousUrl(): Observable { - return of('/item'); - } - }; const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [getIIIFEnabled(true)], 'iiif.search.enabled': [getIIIFSearchEnabled(true)], @@ -214,6 +213,7 @@ describe('UntypedItemComponent', () => { TestBed.overrideProvider(RouteService, {useValue: localMockRouteService}); TestBed.compileComponents(); fixture = TestBed.createComponent(UntypedItemComponent); + comp = fixture.componentInstance; comp.object = getItem(iiifEnabledMap); fixture.detectChanges(); diff --git a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts index 0cedc3bb10..a1f4cebd77 100644 --- a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts +++ b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts @@ -31,13 +31,13 @@ export class VersionedItemComponent extends ItemComponent { private translateService: TranslateService, private versionService: VersionDataService, private itemVersionShared: ItemVersionsSharedService, - private router: Router, + protected router: Router, private workspaceItemDataService: WorkspaceitemDataService, private searchService: SearchService, private itemService: ItemDataService, - protected routeService: RouteService + protected routeService: RouteService, ) { - super(routeService); + super(routeService, router); } /** diff --git a/src/app/shared/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html index 7d9153b9f8..d6a0005173 100644 --- a/src/app/shared/browse-by/browse-by.component.html +++ b/src/app/shared/browse-by/browse-by.component.html @@ -3,7 +3,7 @@
- +
- +