Skip to content

Commit 13f1982

Browse files
committed
[UXP-121] Merge tag 'dspace-cris-2023.02.02' into ux-plus-2023_02_x
Release DSpace-CRIS 2023.02.02 # Conflicts: # src/app/core/core.module.ts # src/assets/i18n/en.json5
2 parents 1d14d84 + ab0d011 commit 13f1982

48 files changed

Lines changed: 696 additions & 106 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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dspace-angular",
3-
"version": "2023.02.02-snapshot",
3+
"version": "2023.02.02",
44
"scripts": {
55
"ng": "ng",
66
"config:watch": "nodemon",

src/app/core/core.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ import {
237237
} from './metadata/schema-json-ld/schema-types/product/product-creative-work-schema-type';
238238
import { ProductDatasetSchemaType } from './metadata/schema-json-ld/schema-types/product/product-dataset-schema-type';
239239
import { PersonSchemaType } from './metadata/schema-json-ld/schema-types/Person/person-schema-type';
240+
import { InternalLinkService } from './services/internal-link.service';
240241
import { UnpaywallItemService } from './data/unpaywall-item.service';
241242

242243
/**
@@ -271,6 +272,7 @@ const PROVIDERS = [
271272
{ provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] },
272273
EPersonDataService,
273274
LinkHeadService,
275+
InternalLinkService,
274276
HALEndpointService,
275277
HostWindowService,
276278
ItemDataService,

src/app/core/data/processes/process-data.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
6868
/**
6969
* Get a process' output files
7070
* @param processId The ID of the process
71+
* @param useCacheVersionIfAvailable If value needs to be red from cache or not
7172
*/
72-
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
73+
getFiles(processId: string, useCacheVersionIfAvailable = true): Observable<RemoteData<PaginatedList<Bitstream>>> {
7374
const href$ = this.getFilesEndpoint(processId);
74-
return this.bitstreamDataService.findListByHref(href$);
75+
return this.bitstreamDataService.findListByHref(href$, {}, useCacheVersionIfAvailable);
7576
}
7677

7778
/**

src/app/core/eperson/group-data.service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { Operation } from 'fast-json-patch';
4141
import { RestRequestMethod } from '../data/rest-request-method';
4242
import { dataService } from '../data/base/data-service.decorator';
4343
import { getGroupEditRoute } from '../../access-control/access-control-routing-paths';
44+
import { isNotEmpty } from '../../shared/empty.util';
4445

4546
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
4647
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
@@ -56,7 +57,7 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
5657
public subgroupsEndpoint = 'subgroups';
5758

5859
private createData: CreateData<Group>;
59-
private searchData: SearchData<Group>;
60+
private searchData: SearchDataImpl<Group>;
6061
private patchData: PatchData<Group>;
6162
private deleteData: DeleteData<Group>;
6263

@@ -91,9 +92,9 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
9192
const options = new FindListOptions();
9293
options.searchParams = [new RequestParam('groupName', groupName)];
9394

94-
return this.searchBy(searchHref, options).pipe(
95+
return this.findByHref(this.searchData.getSearchByHref(searchHref, options)).pipe(
9596
getRemoteDataPayload(),
96-
map((groups: PaginatedList<Group>) => groups.totalElements > 0),
97+
map((group: Group) => isNotEmpty(group)),
9798
catchError(() => observableOf(false)),
9899
);
99100
}

src/app/core/metadata/metadata.service.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ export class MetadataService {
206206
this.setCitationTechnicalReportNumberTag();
207207
}
208208

209+
this.setOpenGraphTitleTag();
210+
this.setOpenGraphDescriptionTag();
211+
this.setOpenGraphImageTag();
212+
213+
this.setTwitterTitleTag();
214+
this.setTwitterDescriptionTag();
215+
this.setTwitterImageTag();
209216
}
210217

211218
/**
@@ -401,22 +408,75 @@ export class MetadataService {
401408
* Add <meta name="citation_pdf_url" ... > to the <head>
402409
*/
403410
private setCitationPdfUrlTag(): void {
411+
this.setPrimaryBitstreamInBundleTag('ORIGINAL', 'citation_pdf_url');
412+
}
413+
414+
/**
415+
* Add <meta name="og:title" ... > to the <head>
416+
*/
417+
private setOpenGraphTitleTag(): void {
418+
const value = this.getMetaTagValue('dc.title');
419+
this.addMetaTag('og:title', value);
420+
}
421+
422+
/**
423+
* Add <meta name="og:description" ... > to the <head>
424+
*/
425+
private setOpenGraphDescriptionTag(): void {
426+
// TODO: truncate abstract
427+
const value = this.getMetaTagValue('dc.description.abstract');
428+
this.addMetaTag('og:description', value);
429+
}
430+
431+
/**
432+
* Add <meta name="og:image" ... > to the <head>
433+
*/
434+
private setOpenGraphImageTag(): void {
435+
this.setPrimaryBitstreamInBundleTag('THUMBNAIL', 'og:image');
436+
}
437+
438+
439+
/**
440+
* Add <meta name="twitter:title" ... > to the <head>
441+
*/
442+
private setTwitterTitleTag(): void {
443+
const value = this.getMetaTagValue('dc.title');
444+
this.addMetaTag('twitter:title', value);
445+
}
446+
447+
/**
448+
* Add <meta name="twitter:description" ... > to the <head>
449+
*/
450+
private setTwitterDescriptionTag(): void {
451+
// TODO: truncate abstract
452+
const value = this.getMetaTagValue('dc.description.abstract');
453+
this.addMetaTag('twitter:description', value);
454+
}
455+
456+
/**
457+
* Add <meta name="twitter:image" ... > to the <head>
458+
*/
459+
private setTwitterImageTag(): void {
460+
this.setPrimaryBitstreamInBundleTag('THUMBNAIL', 'twitter:image');
461+
}
462+
463+
private setPrimaryBitstreamInBundleTag(bundleName: string, tag: string): void {
404464
if (this.currentObject.value instanceof Item) {
405465
const item = this.currentObject.value as Item;
406466

407-
// Retrieve the ORIGINAL bundle for the item
467+
// Retrieve the bundle for the item
408468
this.bundleDataService.findByItemAndName(
409469
item,
410-
'ORIGINAL',
470+
bundleName,
411471
true,
412472
true,
413473
followLink('primaryBitstream'),
414474
followLink('bitstreams', {
415-
findListOptions: {
416-
// limit the number of bitstreams used to find the citation pdf url to the number
417-
// shown by default on an item page
418-
elementsPerPage: this.appConfig.item.bitstream.pageSize
419-
}
475+
findListOptions: {
476+
// limit the number of bitstreams used to find the citation pdf url to the number
477+
// shown by default on an item page
478+
elementsPerPage: this.appConfig.item.bitstream.pageSize
479+
}
420480
}, followLink('format')),
421481
).pipe(
422482
getFirstSucceededRemoteDataPayload(),
@@ -460,7 +520,7 @@ export class MetadataService {
460520
).subscribe((link: string) => {
461521
// Use the found link to set the <meta> tag
462522
this.addMetaTag(
463-
'citation_pdf_url',
523+
tag,
464524
new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString()
465525
);
466526
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { TestBed, waitForAsync } from '@angular/core/testing';
2+
import { InternalLinkService } from './internal-link.service';
3+
import { NativeWindowService } from './window.service';
4+
5+
describe('InternalLinkService', () => {
6+
let service: InternalLinkService;
7+
8+
beforeEach(waitForAsync(() => {
9+
return TestBed.configureTestingModule({
10+
providers: [
11+
InternalLinkService,
12+
{ provide: NativeWindowService, useValue: { nativeWindow: { location: { origin: 'https://currentdomain' } } } },
13+
],
14+
}).compileComponents();
15+
}));
16+
17+
beforeEach(() => {
18+
service = TestBed.inject(InternalLinkService);
19+
});
20+
21+
describe('isLinkInternal', () => {
22+
it('should return true for internal link starting with "/"', () => {
23+
const result = service.isLinkInternal('/my-link');
24+
expect(result).toBe(true);
25+
});
26+
27+
it('should return true for internal link starting with currentURL', () => {
28+
const result = service.isLinkInternal('https://currentdomain/my-link');
29+
expect(result).toBe(true);
30+
});
31+
32+
it('should return true for internal link starting with "currentdomain"', () => {
33+
const result = service.isLinkInternal('currentdomain/my-link');
34+
expect(result).toBe(true);
35+
});
36+
37+
it('should return false for external link', () => {
38+
const result = service.isLinkInternal('https://externaldomain/my-link');
39+
expect(result).toBe(false);
40+
});
41+
42+
it('should return true for internal link without leading "/"', () => {
43+
const result = service.isLinkInternal('my-link');
44+
expect(result).toBe(true);
45+
});
46+
});
47+
48+
describe('transformInternalLink', () => {
49+
it('should transform internal link by removing currentURL', () => {
50+
const result = service.getRelativePath('https://currentdomain/my-link');
51+
expect(result).toBe('/my-link');
52+
});
53+
54+
it('should transform internal link by adding leading "/" if missing', () => {
55+
const result = service.getRelativePath('currentdomain/my-link');
56+
expect(result).toBe('/my-link');
57+
});
58+
59+
it('should return unchanged link for external link', () => {
60+
const result = service.getRelativePath('https://externalDomain/my-link');
61+
expect(result).toBe('https://externalDomain/my-link');
62+
});
63+
64+
it('should return unchanged link for internal link with leading "/"', () => {
65+
const result = service.getRelativePath('/my-link');
66+
expect(result).toBe('/my-link');
67+
});
68+
});
69+
70+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Inject, Injectable } from '@angular/core';
2+
import { NativeWindowRef, NativeWindowService } from './window.service';
3+
4+
/**
5+
* LinkService provides utility functions for working with links, such as checking if a link is internal
6+
* and transforming internal links based on the current URL.
7+
*/
8+
@Injectable()
9+
export class InternalLinkService {
10+
currentURL = this._window.nativeWindow.location.origin;
11+
12+
constructor(
13+
@Inject(NativeWindowService) protected _window: NativeWindowRef,
14+
) {
15+
16+
}
17+
18+
/**
19+
* Check if the provided link is internal, i.e., it starts with a '/' or matches the current URL.
20+
*
21+
* @param link The link to be checked.
22+
* @returns A boolean indicating whether the link is internal.
23+
*/
24+
public isLinkInternal(link: string): boolean {
25+
// Create a Domain object for the provided link
26+
const currentDomain = new URL(this.currentURL).hostname;
27+
28+
return link.startsWith('/')
29+
|| link.startsWith(this.currentURL)
30+
|| link.startsWith(currentDomain)
31+
|| link === currentDomain
32+
|| !link.includes('://');
33+
}
34+
35+
/**
36+
* Get the relative path for an internal link based on the current URL.
37+
*
38+
* @param link The internal link to be transformed.
39+
* @returns The relative path for the given internal link.
40+
*/
41+
public getRelativePath(link: string): string {
42+
// Create a Domain object for the provided link
43+
const currentDomain = new URL(this.currentURL).hostname;
44+
45+
if (link.startsWith(this.currentURL)) {
46+
const currentSegments = link.substring(this.currentURL.length);
47+
return currentSegments.startsWith('/') ? currentSegments : `/${currentSegments}`;
48+
}
49+
50+
if (link.startsWith(currentDomain)) {
51+
const currentSegments = link.substring(currentDomain.length);
52+
return currentSegments.startsWith('/') ? currentSegments : `/${currentSegments}`;
53+
}
54+
55+
return link;
56+
}
57+
}

src/app/core/shared/search/search-configuration.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ export class SearchConfigurationService implements OnDestroy {
164164
*/
165165
getCurrentQuery(defaultQuery: string) {
166166
return this.routeService.getQueryParameterValue('query').pipe(map((query) => {
167-
return query || defaultQuery;
167+
const queryFromURL = query || defaultQuery;
168+
return decodeURIComponent(queryFromURL) ?? '';
168169
}));
169170
}
170171

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('ThumbnailComponent', () => {
152152
}));
153153

154154
it('should show default thumbnail', () => {
155-
expect(component.default).toBe('assets/images/person-placeholder.svg');
155+
expect(component.default).toBe('assets/images/file-placeholder.svg');
156156
});
157157

158158
});
@@ -172,7 +172,7 @@ describe('ThumbnailComponent', () => {
172172
}));
173173

174174
it('should show default thumbnail', () => {
175-
expect(component.default).toBe('assets/images/person-placeholder.svg');
175+
expect(component.default).toBe('assets/images/file-placeholder.svg');
176176
});
177177

178178
});
@@ -239,7 +239,7 @@ describe('ThumbnailComponent', () => {
239239
});
240240

241241
it('should show default thumbnail', () => {
242-
expect(component.default).toBe('assets/images/person-placeholder.svg');
242+
expect(component.default).toBe('assets/images/file-placeholder.svg');
243243
});
244244

245245
});
@@ -254,7 +254,7 @@ describe('ThumbnailComponent', () => {
254254
});
255255

256256
it('should not show bitstream content image src but the default image', () => {
257-
expect(component.default).toBe('assets/images/person-placeholder.svg');
257+
expect(component.default).toBe('assets/images/file-placeholder.svg');
258258
});
259259

260260
});
@@ -269,7 +269,7 @@ describe('ThumbnailComponent', () => {
269269
});
270270

271271
it('should not show thumbnail content image src but the default image', () => {
272-
expect(component.default).toBe('assets/images/person-placeholder.svg');
272+
expect(component.default).toBe('assets/images/file-placeholder.svg');
273273
});
274274

275275
});
@@ -284,7 +284,7 @@ describe('ThumbnailComponent', () => {
284284
});
285285

286286
it('should not show thumbnail content image src but the default image', () => {
287-
expect(component.default).toBe('assets/images/person-placeholder.svg');
287+
expect(component.default).toBe('assets/images/file-placeholder.svg');
288288
});
289289

290290
});

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,13 @@ export class ThumbnailComponent extends BitstreamRenderingModelComponent impleme
9696
*/
9797
setDefaultImage(): void {
9898
const eType = this.item.firstMetadataValue('dspace.entity.type');
99-
this.default = 'assets/images/person-placeholder.svg';
99+
this.default = 'assets/images/file-placeholder.svg';
100100
if (hasValue(eType) && eType.toUpperCase() === 'PROJECT') {
101101
this.default = 'assets/images/project-placeholder.svg';
102102
} else if (hasValue(eType) && eType.toUpperCase() === 'ORGUNIT') {
103103
this.default = 'assets/images/orgunit-placeholder.svg';
104+
} else if (hasValue(eType) && eType.toUpperCase() === 'PERSON') {
105+
this.default = 'assets/images/person-placeholder.svg';
104106
}
105107
}
106108
}

0 commit comments

Comments
 (0)