Skip to content

Commit 8fcc1ba

Browse files
Andrea BarbassoFrancescoMolinaro
authored andcommitted
Merged in task/dspace-cris-2024_02_x/DSC-2889 (pull request DSpace#4607)
Task/dspace cris 2024 02 x/DSC-2889 Approved-by: Francesco Molinaro
2 parents 9140f25 + 5fc5119 commit 8fcc1ba

12 files changed

Lines changed: 530 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
@@ -3,21 +3,26 @@
33
[ngTemplateOutletContext]="{metadataView: metadataView}"></ng-container>
44
</div>
55
<ng-template class="d-flex" #linkToAuthority let-metadataView="metadataView">
6-
<span [dsStickyPopover]="popContent"
6+
<span>
7+
<a rel="noopener noreferrer"
8+
data-test="linkToAuthority"
9+
[routerLink]="[relatedDsoRoute]">
10+
<span>{{metadataView.value}}</span>
11+
</a>
12+
<button [dsStickyPopover]="popContent"
13+
[stickyPopoverClickable]="true"
714
[openDelay]="100"
15+
[closeDelay]="300"
816
[animation]="true"
17+
[placement]="popoverPlacement ?? 'auto'"
918
[autoClose]="true"
10-
container="body"
11-
triggers="mouseenter">
12-
<a rel="noopener noreferrer" data-test="linkToAuthority"
13-
[routerLink]="[relatedDsoRoute]">
19+
class="btn p-0"
20+
[attr.aria-label]="'metadata.link.view.popover-toggle' | translate">
1421
<span dsEntityIcon
1522
[iconPosition]="iconPosition"
1623
[entityType]="metadataView.entityType"
17-
[entityStyle]="metadataView.entityStyle">
18-
{{metadataView.value}}
19-
</span>
20-
</a>
24+
[entityStyle]="metadataView.entityStyle"></span>
25+
</button>
2126
</span>
2227
<img *ngIf="metadataView.orcidAuthenticated"
2328
placement="top"

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
NgbPopoverModule,
1414
NgbTooltipModule,
1515
} from '@ng-bootstrap/ng-bootstrap';
16+
import { TranslateModule } from '@ngx-translate/core';
1617
import {
1718
Observable,
1819
of as observableOf,
@@ -59,6 +60,8 @@ import { StickyPopoverDirective } from './sticky-popover.directive';
5960
NgTemplateOutlet,
6061
AsyncPipe,
6162
StickyPopoverDirective,
63+
TranslateModule,
64+
VarDirective,
6265
],
6366
standalone: true,
6467
})
@@ -78,6 +81,11 @@ export class MetadataLinkViewComponent implements OnInit {
7881
* Item of the metadata value
7982
*/
8083
@Input() item: DSpaceObject;
84+
85+
/**
86+
* Where to place the popover
87+
*/
88+
@Input() popoverPlacement: string;
8189
/**
8290
* The metadata name from where to take the value of the cris style
8391
*/

0 commit comments

Comments
 (0)