diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index f7c02c4262..cbf70ca39a 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -19,6 +19,7 @@ import { CommunityListState } from './community-list.reducer'; import { getCommunityPageRoute } from '../+community-page/community-page-routing-paths'; import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths'; import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators'; +import { followLink } from '../shared/utils/follow-link-config.model'; /** * Each node in the tree is represented by a flatNode which contains info about the node itself and its position and @@ -101,7 +102,7 @@ const communityListStateSelector = (state: AppState) => state.communityList; const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes); const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode); -export const MAX_COMCOLS_PER_PAGE = 50; +export const MAX_COMCOLS_PER_PAGE = 20; /** * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource @@ -115,6 +116,10 @@ export class CommunityListService { private store: Store) { } + private configOnePage: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 1 + }); + saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void { this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode)); } @@ -162,16 +167,19 @@ export class CommunityListService { */ private getTopCommunities(options: FindListOptions): Observable> { return this.communityDataService.findTop({ - currentPage: options.currentPage, - elementsPerPage: MAX_COMCOLS_PER_PAGE, - sort: { - field: options.sort.field, - direction: options.sort.direction - } - }).pipe( - getFirstSucceededRemoteData(), - map((results) => results.payload), - ); + currentPage: options.currentPage, + elementsPerPage: MAX_COMCOLS_PER_PAGE, + sort: { + field: options.sort.field, + direction: options.sort.direction + } + }, + followLink('subcommunities', this.configOnePage, true, true), + followLink('collections', this.configOnePage, true, true)) + .pipe( + getFirstSucceededRemoteData(), + map((results) => results.payload), + ); } /** @@ -231,9 +239,11 @@ export class CommunityListService { let subcoms = []; for (let i = 1; i <= currentCommunityPage; i++) { const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { - elementsPerPage: MAX_COMCOLS_PER_PAGE, - currentPage: i - }) + elementsPerPage: MAX_COMCOLS_PER_PAGE, + currentPage: i + }, + followLink('subcommunities', this.configOnePage, true, true), + followLink('collections', this.configOnePage, true, true)) .pipe( getFirstCompletedRemoteData(), switchMap((rd: RemoteData>) => { @@ -289,7 +299,7 @@ export class CommunityListService { public getIsExpandable(community: Community): Observable { let hasSubcoms$: Observable; let hasColls$: Observable; - hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 }) + hasSubcoms$ = this.communityDataService.findByParent(community.uuid, this.configOnePage) .pipe( map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { @@ -300,7 +310,7 @@ export class CommunityListService { }), ); - hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) + hasColls$ = this.collectionDataService.findByParent(community.uuid, this.configOnePage) .pipe( map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 2dcffeb961..12aedf8009 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -18,6 +18,7 @@ import { BitstreamDataService } from './bitstream-data.service'; import { NoContent } from '../shared/NoContent.model'; import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; @@ -66,11 +67,11 @@ export abstract class ComColDataService extend protected abstract getFindByParentHref(parentUUID: string): Observable; - public findByParent(parentUUID: string, options: FindListOptions = {}): Observable>> { + public findByParent(parentUUID: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { const href$ = this.getFindByParentHref(parentUUID).pipe( map((href: string) => this.buildHrefFromFindOptions(href, options)) ); - return this.findAllByHref(href$); + return this.findAllByHref(href$, options, true, true, ...linksToFollow); } /** diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 6fbc2f9778..8dee72e391 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -19,6 +19,7 @@ import { RemoteData } from './remote-data'; import { FindListOptions } from './request.models'; import { RequestService } from './request.service'; import { BitstreamDataService } from './bitstream-data.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @Injectable() @dataService(COMMUNITY) @@ -45,9 +46,9 @@ export class CommunityDataService extends ComColDataService { return this.halService.getEndpoint(this.linkPath); } - findTop(options: FindListOptions = {}): Observable>> { + findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { const hrefObs = this.getFindAllHref(options, this.topLinkPath); - return this.findAllByHref(hrefObs, undefined); + return this.findAllByHref(hrefObs, undefined, true, true, ...linksToFollow); } protected getFindByParentHref(parentUUID: string): Observable { diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 82d2271c10..a8bbfa79a0 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -193,6 +193,16 @@ describe('DataService', () => { }); }); + it('should include single linksToFollow as embed and its size', () => { + const expected = `${endpoint}?embed.size=bundles=5&embed=bundles`; + const config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 5 + }); + (service as any).getFindAllHref({}, null, followLink('bundles', config, true, true, true)).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + it('should include multiple linksToFollow as embed', () => { const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`; @@ -201,6 +211,18 @@ describe('DataService', () => { }); }); + it('should include multiple linksToFollow as embed and its sizes if given', () => { + const expected = `${endpoint}?embed=bundles&embed.size=owningCollection=2&embed=owningCollection&embed=templateItemOf`; + + const config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 2 + }); + + (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', config, true, true, true), followLink('templateItemOf')).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + it('should not include linksToFollow with shouldEmbed = false', () => { const expected = `${endpoint}?embed=templateItemOf`; @@ -216,6 +238,16 @@ describe('DataService', () => { expect(value).toBe(expected); }); }); + + it('should include nested linksToFollow 2lvl and nested embed\'s size', () => { + const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`; + const config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 4 + }); + (service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', config, true, true, true))).subscribe((value) => { + expect(value).toBe(expected); + }); + }); }); describe('getIDHref', () => { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e98de0f77d..814c281a37 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -215,10 +215,19 @@ export abstract class DataService implements UpdateDa */ protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig[]) { linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { - if (linkToFollow !== undefined && linkToFollow.shouldEmbed) { + if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) { const embedString = 'embed=' + String(linkToFollow.name); - const embedWithNestedString = this.addNestedEmbeds(embedString, ...linkToFollow.linksToFollow); - args = this.addHrefArg(href, args, embedWithNestedString); + // Add the embeds size if given in the FollowLinkConfig.FindListOptions + if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) { + args = this.addHrefArg(href, args, + 'embed.size=' + String(linkToFollow.name) + '=' + linkToFollow.findListOptions.elementsPerPage); + } + // Adds the nested embeds and their size if given + if (isNotEmpty(linkToFollow.linksToFollow)) { + args = this.addNestedEmbeds(embedString, href, args, ...linkToFollow.linksToFollow); + } else { + args = this.addHrefArg(href, args, embedString); + } } }); return args; @@ -243,21 +252,30 @@ export abstract class DataService implements UpdateDa } /** - * Add the nested followLinks to the embed param, recursively, separated by a / + * Add the nested followLinks to the embed param, separated by a /, and their sizes, recursively * @param embedString embedString so far (recursive) + * @param href The href the params are to be added to + * @param args params for the query string * @param linksToFollow links we want to embed in query string if shouldEmbed is true */ - protected addNestedEmbeds(embedString: string, ...linksToFollow: FollowLinkConfig[]): string { + protected addNestedEmbeds(embedString: string, href: string, args: string[], ...linksToFollow: FollowLinkConfig[]): string[] { let nestEmbed = embedString; linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { - if (linkToFollow !== undefined && linkToFollow.shouldEmbed) { + if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) { nestEmbed = nestEmbed + '/' + String(linkToFollow.name); - if (linkToFollow.linksToFollow !== undefined) { - nestEmbed = this.addNestedEmbeds(nestEmbed, ...linkToFollow.linksToFollow); + // Add the nested embeds size if given in the FollowLinkConfig.FindListOptions + if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) { + const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage; + args = this.addHrefArg(href, args, nestedEmbedSize); + } + if (hasValue(linkToFollow.linksToFollow) && isNotEmpty(linkToFollow.linksToFollow)) { + args = this.addNestedEmbeds(nestEmbed, href, args, ...linkToFollow.linksToFollow); + } else { + args = this.addHrefArg(href, args, nestEmbed); } } }); - return nestEmbed; + return args; } /** diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 81c432e31e..2fda0bf40a 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -15,7 +15,8 @@ import { Injectable } from '@angular/core'; import { ResponseParsingService } from './parsing.service'; import { ParsedResponse } from '../cache/response.models'; import { RestRequestMethod } from './rest-request-method'; -import { getUrlWithoutEmbedParams } from '../index/index.selectors'; +import { getUrlWithoutEmbedParams, getEmbedSizeParams } from '../index/index.selectors'; +import { URLCombiner } from '../url-combiner/url-combiner'; /* tslint:disable:max-classes-per-file */ @@ -86,6 +87,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService } public process(data: any, request: RestRequest, alternativeURL?: string): any { + const embedSizeParams = getEmbedSizeParams(request.href); + if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; @@ -100,7 +103,13 @@ export class DspaceRestResponseParsingService implements ResponseParsingService .keys(data._embedded) .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { - this.process(data._embedded[property], request, data._links[property].href); + let embedAltUrl = data._links[property].href; + const match = embedSizeParams + .find((param: { name: string, size: number }) => param.name === property); + if (hasValue(match)) { + embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); + } + this.process(data._embedded[property], request, embedAltUrl); }); } diff --git a/src/app/core/index/index.selectors.spec.ts b/src/app/core/index/index.selectors.spec.ts index 02cce4b7d6..91119a283c 100644 --- a/src/app/core/index/index.selectors.spec.ts +++ b/src/app/core/index/index.selectors.spec.ts @@ -1,4 +1,5 @@ -import { getUrlWithoutEmbedParams } from './index.selectors'; +import { getEmbedSizeParams, getUrlWithoutEmbedParams } from './index.selectors'; + describe(`index selectors`, () => { @@ -29,4 +30,38 @@ describe(`index selectors`, () => { }); + describe(`getEmbedSizeParams`, () => { + + it(`url with single embed size param => should return list with ['subcommunities' - size]`, () => { + const source = 'https://rest.api/core/communities/search/top?page=0&size=50&sort=dc.title,ASC&embed.size=subcommunities=5&embed=subcommunities'; + const result = getEmbedSizeParams(source); + expect(result).toHaveSize(1); + expect(result[0]).toEqual({name: 'subcommunities', size: 5}); + }); + + it(`url with multiple embed size param => should return list with {name, size}`, () => { + const source = 'https://rest.api/core/communities/search/top?page=0&size=50&sort=dc.title,ASC&embed.size=subcommunities=5&embed=subcommunities&embed.size=collections=1&embed=collections'; + const result = getEmbedSizeParams(source); + expect(result).toHaveSize(2); + expect(result[0]).toEqual({name: 'subcommunities', size: 5}); + expect(result[1]).toEqual({name: 'collections', size: 1}); + }); + + it(`url without params => should return empty list`, () => { + const source = 'https://rest.api/core/collections/uuid'; + expect(getEmbedSizeParams(source)).toHaveSize(0); + }); + + it(`url without embed size params => should return empty list`, () => { + const source = 'https://rest.api/core/collections/uuid?page=0&size=50'; + expect(getEmbedSizeParams(source)).toHaveSize(0); + }); + + it(`undefined or null url => should return empty list`, () => { + expect(getEmbedSizeParams(undefined)).toHaveSize(0); + expect(getEmbedSizeParams(null)).toHaveSize(0); + }); + + }); + }); diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts index 40c870b2d1..9b57064388 100644 --- a/src/app/core/index/index.selectors.ts +++ b/src/app/core/index/index.selectors.ts @@ -24,7 +24,7 @@ export const getUrlWithoutEmbedParams = (url: string): string => { if (isNotEmpty(parsed.query)) { const parts = parsed.query.split(/[?|&]/) .filter((part: string) => isNotEmpty(part)) - .filter((part: string) => !part.startsWith('embed=')); + .filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size='))); let args = ''; if (isNotEmpty(parts)) { args = `?${parts.join('&')}`; @@ -37,6 +37,27 @@ export const getUrlWithoutEmbedParams = (url: string): string => { return url; }; +/** + * Parse the embed size params from a url + * @param url The url to parse + */ +export const getEmbedSizeParams = (url: string): { name: string, size: number }[] => { + if (isNotEmpty(url)) { + const parsed = parse(url); + if (isNotEmpty(parsed.query)) { + return parsed.query.split(/[?|&]/) + .filter((part: string) => isNotEmpty(part)) + .map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/)) + .filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2])) + .map((matches: RegExpMatchArray) => { + return { name: matches[1], size: Number(matches[2]) }; + }); + } + } + + return []; +}; + /** * Return the MetaIndexState based on the CoreSate *