Skip to content

Commit 7cfcd2c

Browse files
Andrea BarbassoFrancescoMolinaro
authored andcommitted
Merged in task/dspace-cris-2025_02_x/DSC-2889 (pull request DSpace#4608)
Task/dspace cris 2025 02 x/DSC-2889 Approved-by: Francesco Molinaro
2 parents d3893d3 + 3e01afc commit 7cfcd2c

12 files changed

Lines changed: 528 additions & 21 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
2+
import { ObjectCacheService } from '../cache/object-cache.service';
3+
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
4+
import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service';
5+
import { RestRequest } from './rest-request.model';
6+
import { RestRequestMethod } from './rest-request-method';
7+
8+
class TestService extends DspaceRestResponseParsingService {
9+
constructor(protected objectCache: ObjectCacheService) {
10+
super(objectCache);
11+
}
12+
13+
public ensureSelfLinkForTest(request: RestRequest, response: RawRestResponse): RawRestResponse {
14+
return this.ensureSelfLink(request, response);
15+
}
16+
}
17+
18+
describe('DspaceRestResponseParsingService', () => {
19+
let service: TestService;
20+
21+
beforeEach(() => {
22+
service = new TestService(getMockObjectCacheService());
23+
});
24+
25+
describe('ensureSelfLink', () => {
26+
let warnSpy: jasmine.Spy;
27+
28+
beforeEach(() => {
29+
warnSpy = spyOn(console, 'warn');
30+
});
31+
32+
it('does not replace self link when only query params differ', () => {
33+
const request = {
34+
uuid: 'request-id',
35+
href: 'https://rest.test/server/api/core/items/f639b124-1234-1234-1234-abcdef123456?projection=preventMetadataSecurity',
36+
method: RestRequestMethod.GET,
37+
} as RestRequest;
38+
const response: RawRestResponse = {
39+
payload: {
40+
_links: {
41+
self: {
42+
href: 'https://rest.test/server/api/core/items/f639b124-1234-1234-1234-abcdef123456',
43+
},
44+
},
45+
},
46+
statusCode: 200,
47+
statusText: 'OK',
48+
};
49+
50+
const result = service.ensureSelfLinkForTest(request, response);
51+
52+
expect(result.payload._links.self.href).toBe('https://rest.test/server/api/core/items/f639b124-1234-1234-1234-abcdef123456');
53+
expect(warnSpy).not.toHaveBeenCalled();
54+
});
55+
56+
it('replaces self link when path differs', () => {
57+
const request = {
58+
uuid: 'request-id',
59+
href: 'https://rest.test/server/api/core/items/f639b124-1234-1234-1234-abcdef123456',
60+
method: RestRequestMethod.GET,
61+
} as RestRequest;
62+
const response: RawRestResponse = {
63+
payload: {
64+
_links: {
65+
self: {
66+
href: 'https://rest.test/server/api/core/items/some-other-id',
67+
},
68+
},
69+
},
70+
statusCode: 200,
71+
statusText: 'OK',
72+
};
73+
74+
const result = service.ensureSelfLinkForTest(request, response);
75+
76+
expect(result.payload._links.self.href).toBe('https://rest.test/server/api/core/items/f639b124-1234-1234-1234-abcdef123456');
77+
expect(warnSpy).toHaveBeenCalled();
78+
});
79+
});
80+
});
81+

src/app/core/data/dspace-rest-response-parsing.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export function isRestPaginatedList(halObj: any): boolean {
6060
* @param url the url to split
6161
*/
6262
const splitUrlInParts = (url: string): string[] => {
63-
return url.split('?')
64-
.map((part) => part.split('&'))
65-
.reduce((combined, current) => [...combined, ...current]);
63+
// Compare link structure only, ignoring query params and hash fragments.
64+
const normalizedUrl = url.split('?')[0].split('#')[0];
65+
return normalizedUrl.split('/');
6666
};
6767

6868
@Injectable({ providedIn: 'root' })
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { getDefaultImageUrlByEntityType } from './image.utils';
2+
3+
describe('Image utils', () => {
4+
describe('getDefaultImageUrlByEntityType', () => {
5+
const fallbackImage = 'assets/images/file-placeholder.svg';
6+
7+
it('should return fallback image when entityType is null', (done) => {
8+
getDefaultImageUrlByEntityType(null).subscribe((url) => {
9+
expect(url).toBe(fallbackImage);
10+
done();
11+
});
12+
});
13+
14+
it('should return fallback image when entityType is undefined', (done) => {
15+
getDefaultImageUrlByEntityType(undefined).subscribe((url) => {
16+
expect(url).toBe(fallbackImage);
17+
done();
18+
});
19+
});
20+
21+
it('should return fallback image when entityType is empty string', (done) => {
22+
getDefaultImageUrlByEntityType('').subscribe((url) => {
23+
expect(url).toBe(fallbackImage);
24+
done();
25+
});
26+
});
27+
28+
it('should return the entity-specific placeholder when the image exists', (done) => {
29+
spyOn(window, 'Image').and.returnValue({
30+
set src(_url: string) {
31+
this.onload();
32+
},
33+
onload: null,
34+
onerror: null,
35+
} as any);
36+
37+
getDefaultImageUrlByEntityType('Person').subscribe((url) => {
38+
expect(url).toBe('assets/images/person-placeholder.svg');
39+
done();
40+
});
41+
});
42+
43+
it('should return fallback image when the entity-specific image does not exist', (done) => {
44+
spyOn(window, 'Image').and.returnValue({
45+
set src(_url: string) {
46+
this.onerror();
47+
},
48+
onload: null,
49+
onerror: null,
50+
} as any);
51+
52+
getDefaultImageUrlByEntityType('Person').subscribe((url) => {
53+
expect(url).toBe(fallbackImage);
54+
done();
55+
});
56+
});
57+
58+
it('should lowercase the entityType when building the image path', (done) => {
59+
spyOn(window, 'Image').and.returnValue({
60+
set src(_url: string) {
61+
this.onload();
62+
},
63+
onload: null,
64+
onerror: null,
65+
} as any);
66+
67+
getDefaultImageUrlByEntityType('PUBLICATION').subscribe((url) => {
68+
expect(url).toBe('assets/images/publication-placeholder.svg');
69+
done();
70+
});
71+
});
72+
73+
it('should return fallback image when Image is not defined (SSR)', (done) => {
74+
const originalImage = (window as any).Image;
75+
delete (window as any).Image;
76+
77+
getDefaultImageUrlByEntityType('Person').subscribe((url) => {
78+
expect(url).toBe(fallbackImage);
79+
(window as any).Image = originalImage;
80+
done();
81+
});
82+
});
83+
});
84+
});
85+

src/app/core/shared/image.utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export const getDefaultImageUrlByEntityType = (entityType: string): Observable<s
1616
};
1717

1818
const checkImageExists = (url: string): Observable<boolean> => {
19+
if (typeof Image === 'undefined') {
20+
return of(false);
21+
}
1922
return new Observable<boolean>((observer) => {
2023
const img = new Image();
2124

src/app/shared/metadata-link-view/metadata-link-view.component.html

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@
55
}
66
</div>
77
<ng-template class="d-flex" #linkToAuthority let-metadataView="metadataView">
8-
<span [dsStickyPopover]="popContent"
8+
<span>
9+
<a rel="noopener noreferrer"
10+
data-test="linkToAuthority"
11+
[routerLink]="[relatedDsoRoute]">
12+
<span>{{metadataView.value}}</span>
13+
</a>
14+
<button [dsStickyPopover]="popContent"
15+
[stickyPopoverClickable]="true"
916
[openDelay]="100"
17+
[closeDelay]="300"
1018
[animation]="true"
19+
[placement]="popoverPlacement ?? 'auto'"
1120
[autoClose]="true"
12-
container="body"
13-
triggers="mouseenter">
14-
<a rel="noopener noreferrer" data-test="linkToAuthority"
15-
[routerLink]="[relatedDsoRoute]">
21+
class="btn p-0"
22+
[attr.aria-label]="'metadata.link.view.popover-toggle' | translate">
1623
<span dsEntityIcon
1724
[iconPosition]="iconPosition"
1825
[entityType]="metadataView.entityType"
19-
[entityStyle]="metadataView.entityStyle">
20-
{{metadataView.value}}
21-
</span>
22-
</a>
26+
[entityStyle]="metadataView.entityStyle"></span>
27+
</button>
2328
</span>
2429
@if (metadataView.orcidAuthenticated) {
2530
<img

src/app/shared/metadata-link-view/metadata-link-view.component.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
max-width: 400px !important;
99
width: 100%;
1010
min-width: 300px !important;
11+
.popover-body {
12+
overflow: hidden !important;
13+
}
1114
}

src/app/shared/metadata-link-view/metadata-link-view.component.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
import { VarDirective } from '../utils/var.directive';
2121
import { MetadataLinkViewComponent } from './metadata-link-view.component';
2222
import SpyObj = jasmine.SpyObj;
23+
import { TranslateModule } from '@ngx-translate/core';
24+
2325
import { MetadataLinkViewPopoverComponent } from './metadata-link-view-popover/metadata-link-view-popover.component';
2426

2527
describe('MetadataLinkViewComponent', () => {
@@ -103,9 +105,12 @@ describe('MetadataLinkViewComponent', () => {
103105
beforeEach(waitForAsync(() => {
104106
TestBed.configureTestingModule({
105107
imports: [
108+
TranslateModule.forRoot({}),
106109
NgbTooltipModule,
107110
RouterTestingModule,
108-
MetadataLinkViewComponent, EntityIconDirective, VarDirective,
111+
MetadataLinkViewComponent,
112+
EntityIconDirective,
113+
VarDirective,
109114
],
110115
providers: [
111116
{ provide: ItemDataService, useValue: itemService },

src/app/shared/metadata-link-view/metadata-link-view.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
NgbPopoverModule,
1313
NgbTooltipModule,
1414
} from '@ng-bootstrap/ng-bootstrap';
15+
import { TranslateModule } from '@ngx-translate/core';
1516
import {
1617
Observable,
1718
of,
@@ -56,6 +57,7 @@ import { StickyPopoverDirective } from './sticky-popover.directive';
5657
NgTemplateOutlet,
5758
RouterLink,
5859
StickyPopoverDirective,
60+
TranslateModule,
5961
VarDirective,
6062
],
6163
})
@@ -75,6 +77,11 @@ export class MetadataLinkViewComponent implements OnInit {
7577
* Item of the metadata value
7678
*/
7779
@Input() item: DSpaceObject;
80+
81+
/**
82+
* Where to place the popover
83+
*/
84+
@Input() popoverPlacement: string;
7885
/**
7986
* The metadata name from where to take the value of the cris style
8087
*/

0 commit comments

Comments
 (0)