diff --git a/config/config.example.yml b/config/config.example.yml
index 978ea3b07c7..660b23131a0 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -327,15 +327,6 @@ item:
# The maximum number of values for repeatable metadata to show in the full item
metadataLimit: 20
-# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
-# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests
-# to efficiently display the search results.
-followAuthorityMetadata:
- - type: Publication
- metadata: dc.contributor.author
- - type: Product
- metadata: dc.contributor.author
-
# Collection Page Config
collection:
edit:
@@ -517,3 +508,19 @@ addToAnyPlugin:
title: DSpace CRIS 7 demo
# The link to be shown in the shared post, if different from document.location.origin (optional)
# link: https://dspacecris7.4science.cloud/
+
+# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
+# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests
+# to efficiently display the search results.
+followAuthorityMetadata:
+ - type: Publication
+ metadata: dc.contributor.author
+ - type: Product
+ metadata: dc.contributor.author
+
+# The maximum number of item to process when following authority metadata values.
+followAuthorityMaxItemLimit: 100
+
+# The maximum number of metadata values to process for each metadata key
+# when following authority metadata values.
+followAuthorityMetadataValuesLimit: 5
diff --git a/src/app/core/browse/search-manager.ts b/src/app/core/browse/search-manager.ts
index c8946037304..5afc68c4eea 100644
--- a/src/app/core/browse/search-manager.ts
+++ b/src/app/core/browse/search-manager.ts
@@ -113,7 +113,8 @@ export class SearchManager {
})
.filter((item) => hasValue(item));
- const uuidList = this.extractUUID(items, environment.followAuthorityMetadata);
+ const uuidList = this.extractUUID(items, environment.followAuthorityMetadata, environment.followAuthorityMaxItemLimit);
+
return uuidList.length > 0 ? this.itemService.findAllById(uuidList).pipe(
getFirstCompletedRemoteData(),
map(data => {
@@ -126,20 +127,20 @@ export class SearchManager {
) : of(null);
}
- protected extractUUID(items: Item[], metadataToFollow: FollowAuthorityMetadata[]): string[] {
+ protected extractUUID(items: Item[], metadataToFollow: FollowAuthorityMetadata[], numberOfElementsToReturn?: number): string[] {
const uuidMap = {};
items.forEach((item) => {
metadataToFollow.forEach((followMetadata: FollowAuthorityMetadata) => {
if (item.entityType === followMetadata.type) {
if (isArray(followMetadata.metadata)) {
- followMetadata.metadata.forEach((metadata) => {
- Metadata.all(item.metadata, metadata)
+ followMetadata.metadata.forEach((metadata) => {
+ Metadata.all(item.metadata, metadata, null, environment.followAuthorityMetadataValuesLimit)
.filter((metadataValue: MetadataValue) => Metadata.hasValidItemAuthority(metadataValue.authority))
.forEach((metadataValue: MetadataValue) => uuidMap[metadataValue.authority] = metadataValue);
});
} else {
- Metadata.all(item.metadata, followMetadata.metadata)
+ Metadata.all(item.metadata, followMetadata.metadata, null, environment.followAuthorityMetadataValuesLimit)
.filter((metadataValue: MetadataValue) => Metadata.hasValidItemAuthority(metadataValue.authority))
.forEach((metadataValue: MetadataValue) => uuidMap[metadataValue.authority] = metadataValue);
}
@@ -147,6 +148,10 @@ export class SearchManager {
});
});
+ if (hasValue(numberOfElementsToReturn) && numberOfElementsToReturn > 0) {
+ return Object.keys(uuidMap).slice(0, numberOfElementsToReturn);
+ }
+
return Object.keys(uuidMap);
}
}
diff --git a/src/app/core/browse/search.manager.spec.ts b/src/app/core/browse/search.manager.spec.ts
index da9097be76f..e319a22933e 100644
--- a/src/app/core/browse/search.manager.spec.ts
+++ b/src/app/core/browse/search.manager.spec.ts
@@ -182,6 +182,11 @@ describe('SearchManager', () => {
const uuidList = (service as any).extractUUID([firstPublication, firstPublication], [{type: 'Publication', metadata: ['dc.contributor.author']}]);
expect(uuidList).toEqual([validAuthority]);
});
+
+ it('should limit the number of extracted uuids', () => {
+ const uuidList = (service as any).extractUUID([firstPublication, secondPublication, invalidAuthorityPublication], [{type: 'Publication', metadata: ['dc.contributor.author']}], 2);
+ expect(uuidList.length).toBe(2);
+ });
});
});
diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts
index c0cc8d9c214..717988b4aae 100644
--- a/src/app/core/shared/dspace-object.model.ts
+++ b/src/app/core/shared/dspace-object.model.ts
@@ -115,6 +115,18 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
return Metadata.all(this.metadata, keyOrKeys, valueFilter);
}
+ /**
+ * Gets all matching metadata in this DSpaceObject, up to a limit.
+ *
+ * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
+ * @param {number} limit The maximum number of results to return.
+ * @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done.
+ * @returns {MetadataValue[]} the matching values or an empty array.
+ */
+ limitedMetadata(keyOrKeys: string | string[], limit: number, valueFilter?: MetadataValueFilter): MetadataValue[] {
+ return Metadata.all(this.metadata, keyOrKeys, valueFilter, limit);
+ }
+
/**
* Like [[allMetadata]], but only returns string values.
*
diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts
index a80e482f24f..f4348723001 100644
--- a/src/app/core/shared/metadata.utils.spec.ts
+++ b/src/app/core/shared/metadata.utils.spec.ts
@@ -44,11 +44,11 @@ const multiViewModelList = [
{ key: 'foo', ...bar, order: 0 }
];
-const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => {
+const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?, limit?: number) => {
const keys = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys];
describe('and key' + (keys.length === 1 ? (' ' + keys[0]) : ('s ' + JSON.stringify(keys)))
+ ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => {
- const result = fn(mapOrMaps, keys, filter);
+ const result = fn(mapOrMaps, keys, filter, limit);
let shouldReturn;
if (resultKind === 'boolean') {
shouldReturn = expected;
@@ -56,7 +56,8 @@ const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) =>
shouldReturn = 'undefined';
} else if (expected instanceof Array) {
shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '')
- + resultKind + (expected.length !== 1 ? 's' : '');
+ + resultKind + (expected.length !== 1 ? 's' : '')
+ + (isUndefined(limit) ? '' : ' (limited to ' + limit + ')');
} else {
shouldReturn = 'a ' + resultKind;
}
@@ -297,4 +298,12 @@ describe('Metadata', () => {
});
+ describe('all method with limit', () => {
+ const testAllWithLimit = (mapOrMaps, keyOrKeys, expected, limit) =>
+ testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, undefined, limit);
+
+ describe('with multiMap and limit', () => {
+ testAllWithLimit(multiMap, 'dc.title', [dcTitle1], 1);
+ });
+ });
});
diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts
index f6a7fb4fd00..ae5f85f582a 100644
--- a/src/app/core/shared/metadata.utils.ts
+++ b/src/app/core/shared/metadata.utils.ts
@@ -37,10 +37,11 @@ export class Metadata {
* checked in order, and only values from the first with at least one match will be returned.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
+ * @param {number} limit The maximum number of values to return. If unspecified, all matching values will be returned.
* @returns {MetadataValue[]} the matching values or an empty array.
*/
public static all(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[],
- filter?: MetadataValueFilter): MetadataValue[] {
+ filter?: MetadataValueFilter, limit?: number): MetadataValue[] {
const mdMaps: MetadataMapInterface[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps];
const matches: MetadataValue[] = [];
for (const mdMap of mdMaps) {
@@ -50,6 +51,9 @@ export class Metadata {
for (const candidate of candidates) {
if (Metadata.valueMatches(candidate as MetadataValue, filter)) {
matches.push(candidate as MetadataValue);
+ if (hasValue(limit) && matches.length >= limit) {
+ return matches;
+ }
}
}
}
diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html
index 0fd99735946..e38a8baf492 100644
--- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html
+++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html
@@ -22,11 +22,20 @@
)
{{'mydspace.results.no-authors' | translate}}
-
- ;
-
+
+
+ ;
+
+
+
+
+ ;
+
+
diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts
index 24b57dbfbc3..3e6941f4fd7 100644
--- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts
+++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts
@@ -12,6 +12,7 @@ import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../../utils/var.directive';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
+import { TruncatableService } from '../../../truncatable/truncatable.service';
let component: ItemListPreviewComponent;
let fixture: ComponentFixture;
@@ -80,6 +81,10 @@ const enviromentNoThumbs = {
}
};
+const truncatableServiceStub: any = {
+ isCollapsed: (id: number) => observableOf(true),
+};
+
describe('ItemListPreviewComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
@@ -95,7 +100,8 @@ describe('ItemListPreviewComponent', () => {
declarations: [ItemListPreviewComponent, TruncatePipe, VarDirective],
providers: [
{ provide: 'objectElementProvider', useValue: { mockItemWithAuthorAndDate }},
- { provide: APP_CONFIG, useValue: environmentUseThumbs }
+ { provide: APP_CONFIG, useValue: environmentUseThumbs },
+ { provide: TruncatableService, useValue: truncatableServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
@@ -198,6 +204,33 @@ describe('ItemListPreviewComponent', () => {
expect(entityField).toBeNull();
});
});
+
+
+ describe('When truncatable section is collapsed', () => {
+ beforeEach(() => {
+ component.isCollapsed$ = observableOf(true);
+ component.item = mockItemWithAuthorAndDate;
+ fixture.detectChanges();
+ });
+
+ it('should show limitedMetadata', () => {
+ const authorElements = fixture.debugElement.queryAll(By.css('span.item-list-authors ds-metadata-link-view'));
+ expect(authorElements.length).toBe(mockItemWithAuthorAndDate.limitedMetadata(component.authorMetadata, component.authorMetadataLimit).length);
+ });
+ });
+
+ describe('When truncatable section is expanded', () => {
+ beforeEach(() => {
+ component.isCollapsed$ = observableOf(false);
+ component.item = mockItemWithAuthorAndDate;
+ fixture.detectChanges();
+ });
+
+ it('should show allMetadata', () => {
+ const authorElements = fixture.debugElement.queryAll(By.css('span.item-list-authors ds-metadata-link-view'));
+ expect(authorElements.length).toBe(mockItemWithAuthorAndDate.allMetadata(component.authorMetadata).length);
+ });
+ });
});
describe('ItemListPreviewComponent', () => {
@@ -215,7 +248,8 @@ describe('ItemListPreviewComponent', () => {
declarations: [ItemListPreviewComponent, TruncatePipe],
providers: [
{provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}},
- {provide: APP_CONFIG, useValue: enviromentNoThumbs}
+ {provide: APP_CONFIG, useValue: enviromentNoThumbs},
+ { provide: TruncatableService, useValue: truncatableServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts
index a4049936deb..de53fef63dc 100644
--- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts
+++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts
@@ -12,6 +12,8 @@ import {
} from '../../../../submission/sections/detect-duplicate/models/duplicate-detail-metadata.model';
import { parseISO, differenceInDays, differenceInMilliseconds } from 'date-fns';
import { environment } from '../../../../../environments/environment';
+import { TruncatableService } from '../../../truncatable/truncatable.service';
+import { Observable } from 'rxjs';
/**
* This component show metadata for the given item object in the list view.
@@ -78,9 +80,14 @@ export class ItemListPreviewComponent implements OnInit {
authorMetadata = environment.searchResult.authorMetadata;
+ authorMetadataLimit = environment.followAuthorityMetadataValuesLimit;
+
+ isCollapsed$: Observable;
+
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
public dsoNameService: DSONameService,
+ public truncateService: TruncatableService
) {
}
@@ -96,6 +103,6 @@ export class ItemListPreviewComponent implements OnInit {
ngOnInit(): void {
this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails;
this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.item);
+ this.isCollapsed$ = this.truncateService.isCollapsed(this.item.uuid);
}
-
}
diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html
index 990166ab51a..521e6d5bd1a 100644
--- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html
+++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html
@@ -39,9 +39,18 @@
[innerHTML]="firstMetadataValue('dc.date.issued')">)
0" class="item-list-authors">
-
- ;
-
+
+
+
+ ;
+
+
+
+
+ ;
+
+
+
diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts
index 35654240a5d..e44aadb7a5b 100644
--- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts
+++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts
@@ -186,13 +186,17 @@ const enviromentNoThumbs = {
}
};
+const truncatableServiceStub = {
+ isCollapsed: (id: number) => observableOf(true),
+};
+
describe('ItemSearchResultListElementComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [ItemSearchResultListElementComponent, TruncatePipe, VarDirective],
providers: [
- { provide: TruncatableService, useValue: {} },
+ { provide: TruncatableService, useValue: truncatableServiceStub },
{ provide: DSONameService, useClass: DSONameServiceMock },
{ provide: APP_CONFIG, useValue: environmentUseThumbs }
],
@@ -247,6 +251,32 @@ describe('ItemSearchResultListElementComponent', () => {
});
});
+ describe('When the item has authors and isCollapsed is true', () => {
+ beforeEach(() => {
+ spyOn(publicationListElementComponent, 'isCollapsed').and.returnValue(observableOf(true));
+ publicationListElementComponent.object = mockItemWithMetadata;
+ fixture.detectChanges();
+ });
+
+ it('should show limitedMetadata', () => {
+ const authorElements = fixture.debugElement.queryAll(By.css('span.item-list-authors ds-metadata-link-view'));
+ expect(authorElements.length).toBe(mockItemWithMetadata.indexableObject.limitedMetadata(publicationListElementComponent.authorMetadata, publicationListElementComponent.additionalMetadataLimit).length);
+ });
+ });
+
+ describe('When the item has authors and isCollapsed is false', () => {
+ beforeEach(() => {
+ spyOn(publicationListElementComponent, 'isCollapsed').and.returnValue(observableOf(false));
+ publicationListElementComponent.object = mockItemWithMetadata;
+ fixture.detectChanges();
+ });
+
+ it('should show allMetadata', () => {
+ const authorElements = fixture.debugElement.queryAll(By.css('span.item-list-authors ds-metadata-link-view'));
+ expect(authorElements.length).toBe(mockItemWithMetadata.indexableObject.allMetadata(publicationListElementComponent.authorMetadata).length);
+ });
+ });
+
describe('When the item has a publisher', () => {
beforeEach(() => {
publicationListElementComponent.object = mockItemWithMetadata;
@@ -375,7 +405,7 @@ describe('ItemSearchResultListElementComponent', () => {
imports: [TranslateModule.forRoot()],
declarations: [ItemSearchResultListElementComponent, TruncatePipe],
providers: [
- {provide: TruncatableService, useValue: {}},
+ {provide: TruncatableService, useValue: truncatableServiceStub},
{provide: DSONameService, useClass: DSONameServiceMock},
{ provide: APP_CONFIG, useValue: enviromentNoThumbs }
],
diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts
index e55ea5b14a5..aeeb0e912b2 100644
--- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts
+++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts
@@ -11,6 +11,7 @@ import { TruncatableService } from '../../truncatable/truncatable.service';
import { Metadata } from '../../../core/shared/metadata.utils';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
+import { environment } from '../../../../environments/environment';
@Component({
selector: 'ds-search-result-list-element',
@@ -23,6 +24,11 @@ export class SearchResultListElementComponent, K exten
dso: K;
dsoTitle: string;
+ /**
+ * Limit of additional metadata values to show
+ */
+ additionalMetadataLimit = environment.followAuthorityMetadataValuesLimit;
+
public constructor(protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {
diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts
index 589999d4746..1488c1c173e 100644
--- a/src/config/app-config.interface.ts
+++ b/src/config/app-config.interface.ts
@@ -70,6 +70,8 @@ interface AppConfig extends Config {
suggestion: SuggestionConfig[];
addToAnyPlugin: AddToAnyPluginConfig;
followAuthorityMetadata: FollowAuthorityMetadata[];
+ followAuthorityMaxItemLimit: number;
+ followAuthorityMetadataValuesLimit: number;
metricVisualizationConfig: MetricVisualizationConfig[];
attachmentRendering: AttachmentRenderingConfig;
advancedAttachmentRendering: AdvancedAttachmentRenderingConfig;
diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts
index 2c8ebabf6b4..99234d781e8 100644
--- a/src/config/default-app-config.ts
+++ b/src/config/default-app-config.ts
@@ -404,6 +404,12 @@ export class DefaultAppConfig implements AppConfig {
}
];
+ // The maximum number of item to process when following authority metadata values.
+ followAuthorityMaxItemLimit = 100;
+ // The maximum number of metadata values to process for each metadata key
+ // when following authority metadata values.
+ followAuthorityMetadataValuesLimit = 5;
+
// Collection Page Config
collection: CollectionPageConfig = {
edit: {
diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts
index a47ca41cf83..c7df8a62c44 100644
--- a/src/environments/environment.test.ts
+++ b/src/environments/environment.test.ts
@@ -276,6 +276,8 @@ export const environment: BuildConfig = {
metadata: ['dc.contributor.author']
}
],
+ followAuthorityMaxItemLimit: 100,
+ followAuthorityMetadataValuesLimit: 5,
item: {
edit: {
undoTimeout: 10000 // 10 seconds