Skip to content

Commit e0bb5f9

Browse files
Merge branch 'task/main/DURACOM-444' into task/main/DURACOM-453
2 parents a3c14c5 + a51e5c2 commit e0bb5f9

222 files changed

Lines changed: 8094 additions & 841 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

config/config.example.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ submission:
238238
style: text-muted
239239
icon: fa-circle-xmark
240240

241+
# Icons to be displayed next to an authority controlled value, to give indication of the source.
242+
sourceIcons:
243+
# Example of configuration for authority logo based on sources.
244+
# The condigured icon will be displayed next to the authority value in submission and on item page or search results.
245+
- source: orcid
246+
- path: assets/images/orcid.logo.icon.svg
241247
# Fallback language in which the UI will be rendered if the user's browser language is not an active language
242248
fallbackLanguage: en
243249

@@ -642,3 +648,98 @@ geospatialMapViewer:
642648
accessibility:
643649
# The duration in days after which the accessibility settings cookie expires
644650
cookieExpirationDuration: 7
651+
652+
# Configuration for custom layout
653+
layout:
654+
# Configuration of icons and styles to be used for each authority controlled link
655+
authorityRef:
656+
- entityType: DEFAULT
657+
entityStyle:
658+
default:
659+
icon: fa fa-user
660+
style: text-info
661+
- entityType: PERSON
662+
entityStyle:
663+
person:
664+
icon: fa fa-user
665+
style: text-success
666+
default:
667+
icon: fa fa-user
668+
style: text-info
669+
- entityType: ORGUNIT
670+
entityStyle:
671+
default:
672+
icon: fa fa-university
673+
style: text-success
674+
- entityType: PROJECT
675+
entityStyle:
676+
default:
677+
icon: fas fa-project-diagram
678+
style: text-success
679+
680+
# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
681+
# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests
682+
# to efficiently display the search results.
683+
followAuthorityMetadata:
684+
- type: Publication
685+
metadata: dc.contributor.author
686+
- type: Product
687+
metadata: dc.contributor.author
688+
689+
# The maximum number of item to process when following authority metadata values.
690+
followAuthorityMaxItemLimit: 100
691+
692+
# The maximum number of metadata values to process for each metadata key
693+
# when following authority metadata values.
694+
followAuthorityMetadataValuesLimit: 5
695+
696+
# Configuration for customization of search results
697+
searchResults:
698+
# Metadata fields to be displayed in the search results under the standard ones
699+
additionalMetadataFields:
700+
- dc.contributor.author
701+
- dc.date.issued
702+
- dc.type
703+
# Metadata fields to be displayed in the search results for the author section
704+
authorMetadata:
705+
- dc.contributor.author
706+
- dc.creator
707+
- dc.contributor.*
708+
709+
# Configuration of metadata to be displayed in the item metadata link view popover
710+
metadataLinkViewPopoverData:
711+
# Metdadata list to be displayed for entities without a specific configuration
712+
fallbackMetdataList:
713+
- dc.description.abstract
714+
- dc.description.note
715+
# Configuration for each entity type
716+
entityDataConfig:
717+
- entityType: Person
718+
# Descriptive metadata (popover body)
719+
metadataList:
720+
- person.affiliation.name
721+
- person.email
722+
# Title metadata (popover header)
723+
titleMetadataList:
724+
- person.givenName
725+
- person.familyName
726+
# Configuration for identifier subtypes, based on metadata like dc.identifier.ror where ror is the subtype.
727+
# This is used to map the layout of the identifier in the popover and the icon displayed next to the metadata value.
728+
identifierSubtypes:
729+
- name: ror
730+
icon: assets/images/ror.logo.icon.svg
731+
iconPosition: IdentifierSubtypesIconPositionEnum.LEFT
732+
link: https://ror.org
733+
734+
# The maximum number of item to process when following authority metadata values.
735+
followAuthorityMaxItemLimit: 100
736+
737+
# The maximum number of metadata values to process for each metadata key
738+
# when following authority metadata values.
739+
followAuthorityMetadataValuesLimit: 5;
740+
741+
# When the search results are retrieved, for each item type the metadata with a valid authority value are inspected.
742+
# Referenced items will be fetched with a find all by id strategy to avoid individual rest requests to efficiently display the search results.
743+
followAuthorityMetadata:
744+
- type: Publication
745+
metadata: dc.contributor.author

scripts/sync-i18n-files.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,11 @@ function parseCliInput() {
3737
.option('-o, --output-file <output>', 'where output of script ends up; mutually exclusive with -i')
3838
.usage('([-d <output-dir>] [-s <source-file>]) || (-t <target-file> (-i | -o <output>) [-s <source-file>])')
3939
.parse(process.argv);
40-
41-
const sourceFile = program.opts().sourceFile;
42-
43-
if (!program.targetFile) {
40+
if (!program.targetFile) {
4441
fs.readdirSync(projectRoot(LANGUAGE_FILES_LOCATION)).forEach(file => {
45-
if (!sourceFile.toString().endsWith(file)) {
42+
if (program.opts().sourceFile && !program.opts().sourceFile.toString().endsWith(file)) {
4643
const targetFileLocation = projectRoot(LANGUAGE_FILES_LOCATION + "/" + file);
47-
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + sourceFile);
44+
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + program.opts().sourceFile);
4845
if (program.outputDir) {
4946
if (!fs.existsSync(program.outputDir)) {
5047
fs.mkdirSync(program.outputDir);
@@ -69,7 +66,7 @@ function parseCliInput() {
6966
console.log(program.outputHelp());
7067
process.exit(1);
7168
}
72-
if (!checkIfFileExists(sourceFile)) {
69+
if (!checkIfFileExists(program.opts().sourceFile)) {
7370
console.error('Path of source file is not valid.');
7471
console.log(program.outputHelp());
7572
process.exit(1);

src/app/audit-page/object-audit-overview/object-audit-logs.component.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ describe('ObjectAuditLogsComponent', () => {
6060
{ findOwningCollectionFor: createSuccessfulRemoteDataObject$(createPaginatedList([{ id : 'collectionId' }])) },
6161
);
6262
activatedRoute = new MockActivatedRoute({ objectId: mockItemId });
63-
activatedRoute.paramMap = of({
64-
get: () => mockItemId,
65-
});
63+
activatedRoute.data = of({ dso: {
64+
payload: mockItem,
65+
} });
6666
locationStub = jasmine.createSpyObj('location', {
6767
back: jasmine.createSpy('back'),
6868
});

src/app/audit-page/object-audit-overview/object-audit-logs.component.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@angular/core';
99
import {
1010
ActivatedRoute,
11-
ParamMap,
11+
Data,
1212
Router,
1313
RouterLink,
1414
} from '@angular/router';
@@ -111,9 +111,8 @@ export class ObjectAuditLogsComponent implements OnInit {
111111
) {}
112112

113113
ngOnInit(): void {
114-
this.objectId$ = this.route.paramMap.pipe(
115-
map((paramMap: ParamMap) => paramMap.get('id')),
116-
switchMap((id: string) => this.dSpaceObjectDataService.findById(id, true, true)),
114+
this.objectId$ = this.route.data.pipe(
115+
switchMap((data: Data) => this.dSpaceObjectDataService.findById(data.dso.payload.id, true, true)),
117116
getFirstSucceededRemoteDataPayload(),
118117
tap((object) => {
119118
this.objectRoute = getDSORoute(object);

src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class BrowseByTaxonomyComponent implements OnInit, OnChanges, OnDestroy {
128128
this.selectedItems = [];
129129
this.facetType = browseDefinition.facetType;
130130
this.vocabularyName = browseDefinition.vocabulary;
131-
this.vocabularyOptions = { name: this.vocabularyName, closed: true };
131+
this.vocabularyOptions = { name: this.vocabularyName, metadata: null, scope: null, closed: true };
132132
this.description = this.translate.instant(`browse.metadata.${this.vocabularyName}.tree.description`);
133133
}));
134134
this.subs.push(this.scope$.subscribe(() => {

src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Observable } from 'rxjs';
77
import { map } from 'rxjs/operators';
88

99
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
10+
import { ItemDataService } from '../data/item-data.service';
1011
import { getDSORoute } from '../router/utils/dso-route.utils';
1112
import { DSpaceObject } from '../shared/dspace-object.model';
1213
import { FollowLinkConfig } from '../shared/follow-link-config.model';
@@ -55,7 +56,9 @@ export const DSOBreadcrumbResolverByUuid: (route: ActivatedRouteSnapshot, state:
5556
dataService: IdentifiableDataService<DSpaceObject>,
5657
...linksToFollow: FollowLinkConfig<DSpaceObject>[]
5758
): Observable<BreadcrumbConfig<DSpaceObject>> => {
58-
return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
59+
const isItemDataService = dataService instanceof ItemDataService;
60+
const findMethod = isItemDataService ? dataService.findByIdOrCustomUrl.bind(dataService) : dataService.findById.bind(dataService);
61+
return findMethod(uuid, true, false, ...linksToFollow).pipe(
5962
getFirstCompletedRemoteData(),
6063
getRemoteDataPayload(),
6164
map((object: DSpaceObject) => {

src/app/core/data/external-source-data.service.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ export class ExternalSourceDataService extends IdentifiableDataService<ExternalS
6666
);
6767
}
6868

69+
/**
70+
* Get the endpoint for an external source's entryValues by given entry id
71+
* @param externalSourceId The id of the external source to fetch entry for
72+
* @param entryId The id of the external source entry to retrieve
73+
*/
74+
getEntryIDHref(externalSourceId: string, entryId: string): Observable<string> {
75+
return this.getBrowseEndpoint().pipe(
76+
map((href) => href + '/' + externalSourceId + '/entryValues/' + entryId),
77+
);
78+
}
79+
6980
/**
7081
* Get the entries for an external source
7182
* @param externalSourceId The id of the external source to fetch entries for
@@ -111,4 +122,18 @@ export class ExternalSourceDataService extends IdentifiableDataService<ExternalS
111122
public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<ExternalSource>[]): Observable<RemoteData<PaginatedList<ExternalSource>>> {
112123
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
113124
}
125+
126+
/**
127+
* Get an entry for an external source by given entry id
128+
* @param externalSourceId The id of the external source to fetch entries for
129+
* @param entryId The id of the entry to retrieve
130+
*/
131+
getExternalSourceEntryById(externalSourceId: string, entryId: string): Observable<RemoteData<ExternalSourceEntry>> {
132+
const href$ = this.getEntryIDHref(externalSourceId, entryId).pipe(
133+
isNotEmptyOperator(),
134+
distinctUntilChanged(),
135+
);
136+
137+
return this.findByHref(href$) as any;
138+
}
114139
}

src/app/core/data/item-data.service.spec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HttpClient } from '@angular/common/http';
2+
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
23
import { Store } from '@ngrx/store';
34
import {
45
cold,
@@ -13,6 +14,7 @@ import { RestResponse } from '../cache/response.models';
1314
import { CoreState } from '../core-state.model';
1415
import { NotificationsService } from '../notification-system/notifications.service';
1516
import { ExternalSourceEntry } from '../shared/external-source-entry.model';
17+
import { Item } from '../shared/item.model';
1618
import { HALEndpointServiceStub } from '../testing/hal-endpoint-service.stub';
1719
import { getMockRemoteDataBuildService } from '../testing/remote-data-build.service.mock';
1820
import { getMockRequestService } from '../testing/request.service.mock';
@@ -209,4 +211,93 @@ describe('ItemDataService', () => {
209211
});
210212
});
211213

214+
describe('findByCustomUrl', () => {
215+
let itemDataService: ItemDataService;
216+
let searchData: any;
217+
let findByHrefSpy: jasmine.Spy;
218+
let getSearchByHrefSpy: jasmine.Spy;
219+
const id = 'custom-id';
220+
const fakeHrefObs = of('https://rest.api/core/items/search/findByCustomURL?q=custom-id');
221+
const linksToFollow = [];
222+
const projections = ['full', 'detailed'];
223+
224+
beforeEach(() => {
225+
searchData = jasmine.createSpyObj('searchData', ['getSearchByHref']);
226+
getSearchByHrefSpy = searchData.getSearchByHref.and.returnValue(fakeHrefObs);
227+
itemDataService = new ItemDataService(
228+
requestService,
229+
rdbService,
230+
objectCache,
231+
halEndpointService,
232+
notificationsService,
233+
comparator,
234+
browseService,
235+
bundleService,
236+
);
237+
238+
(itemDataService as any).searchData = searchData;
239+
findByHrefSpy = spyOn(itemDataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
240+
});
241+
242+
it('should call searchData.getSearchByHref with correct parameters', () => {
243+
itemDataService.findByCustomUrl(id, true, true, linksToFollow, projections).subscribe();
244+
245+
expect(getSearchByHrefSpy).toHaveBeenCalledWith(
246+
'findByCustomURL',
247+
jasmine.objectContaining({
248+
searchParams: jasmine.arrayContaining([
249+
jasmine.objectContaining({ fieldName: 'q', fieldValue: id }),
250+
jasmine.objectContaining({ fieldName: 'projection', fieldValue: 'full' }),
251+
jasmine.objectContaining({ fieldName: 'projection', fieldValue: 'detailed' }),
252+
]),
253+
}),
254+
...linksToFollow,
255+
);
256+
});
257+
258+
it('should call findByHref with the href observable returned from getSearchByHref', () => {
259+
itemDataService.findByCustomUrl(id, true, false, linksToFollow, projections).subscribe();
260+
261+
expect(findByHrefSpy).toHaveBeenCalledWith(fakeHrefObs, true, false, ...linksToFollow);
262+
});
263+
});
264+
265+
describe('findById', () => {
266+
let itemDataService: ItemDataService;
267+
268+
beforeEach(() => {
269+
itemDataService = new ItemDataService(
270+
requestService,
271+
rdbService,
272+
objectCache,
273+
halEndpointService,
274+
notificationsService,
275+
comparator,
276+
browseService,
277+
bundleService,
278+
);
279+
spyOn(itemDataService, 'findByCustomUrl').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
280+
spyOn(itemDataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(new Item()));
281+
spyOn(itemDataService as any, 'getIDHrefObs').and.returnValue(of('uuid-href'));
282+
});
283+
284+
it('should call findByHref when given a valid UUID', () => {
285+
const validUuid = '4af28e99-6a9c-4036-a199-e1b587046d39';
286+
itemDataService.findById(validUuid).subscribe();
287+
288+
expect((itemDataService as any).getIDHrefObs).toHaveBeenCalledWith(encodeURIComponent(validUuid));
289+
expect(itemDataService.findByHref).toHaveBeenCalled();
290+
expect(itemDataService.findByCustomUrl).not.toHaveBeenCalled();
291+
});
292+
293+
it('should call findByCustomUrl when given a non-UUID id', () => {
294+
const nonUuid = 'custom-url';
295+
itemDataService.findByIdOrCustomUrl(nonUuid).subscribe();
296+
297+
expect(itemDataService.findByCustomUrl).toHaveBeenCalledWith(nonUuid, true, true, []);
298+
expect(itemDataService.findByHref).not.toHaveBeenCalled();
299+
});
300+
});
301+
302+
212303
});

0 commit comments

Comments
 (0)