Skip to content

Commit 2970646

Browse files
committed
Adiciona endpoint de bibliografia baseado em CSL para itens
PR do DSpace: DSpace/dspace-angular#4779
1 parent 3625978 commit 2970646

File tree

20 files changed

+419
-5
lines changed

20 files changed

+419
-5
lines changed

src/app/core/data-services-map.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ORCID_QUEUE } from './orcid/model/orcid-queue.resource-type';
3838
import { RESEARCHER_PROFILE } from './profile/model/researcher-profile.resource-type';
3939
import { RESOURCE_POLICY } from './resource-policy/models/resource-policy.resource-type';
4040
import { AUTHORIZATION } from './shared/authorization.resource-type';
41+
import { BIBLIOGRAPHY } from './shared/bibliography/bibliography.resource-type';
4142
import { BITSTREAM } from './shared/bitstream.resource-type';
4243
import { BITSTREAM_FORMAT } from './shared/bitstream-format.resource-type';
4344
import { BROWSE_DEFINITION } from './shared/browse-definition.resource-type';
@@ -88,6 +89,7 @@ export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([
8889
[SUBSCRIPTION.value, () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService)],
8990
[COMMUNITY.value, () => import('./data/community-data.service').then(m => m.CommunityDataService)],
9091
[VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)],
92+
[BIBLIOGRAPHY.value, () => import('./data/bibliography-data.service').then(m => m.ItemBibliographyService)],
9193
[BUNDLE.value, () => import('./data/bundle-data.service').then(m => m.BundleDataService)],
9294
[CONFIG_PROPERTY.value, () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService)],
9395
[POOL_TASK.value, () => import('./tasks/pool-task-data.service').then(m => m.PoolTaskDataService)],
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Injectable } from '@angular/core';
2+
import {
3+
map,
4+
Observable,
5+
} from 'rxjs';
6+
7+
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
8+
import { ObjectCacheService } from '../cache/object-cache.service';
9+
import { BibliographyData } from '../shared/bibliography/bibliography-data.model';
10+
import { HALEndpointService } from '../shared/hal-endpoint.service';
11+
import { Item } from '../shared/item.model';
12+
import { getFirstCompletedRemoteData } from '../shared/operators';
13+
import { BaseDataService } from './base/base-data.service';
14+
import { RequestService } from './request.service';
15+
16+
17+
18+
19+
@Injectable({ providedIn: 'root' })
20+
export class ItemBibliographyService extends BaseDataService<BibliographyData> {
21+
22+
constructor(
23+
protected requestService: RequestService,
24+
protected rdbService: RemoteDataBuildService,
25+
protected objectCache: ObjectCacheService,
26+
protected halService: HALEndpointService,
27+
) {
28+
super('bibliographies', requestService, rdbService, objectCache, halService);
29+
}
30+
31+
getBibliographies(item: Item): Observable<BibliographyData> {
32+
return this.findByHref(item._links.bibliography.href).pipe(
33+
getFirstCompletedRemoteData(),
34+
map(res => res.payload),
35+
);
36+
}
37+
}

src/app/core/provide-core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { OrcidQueue } from './orcid/model/orcid-queue.model';
4141
import { ResearcherProfile } from './profile/model/researcher-profile.model';
4242
import { ResourcePolicy } from './resource-policy/models/resource-policy.model';
4343
import { Authorization } from './shared/authorization.model';
44+
import { BibliographyData } from './shared/bibliography/bibliography-data.model';
4445
import { Bitstream } from './shared/bitstream.model';
4546
import { BitstreamFormat } from './shared/bitstream-format.model';
4647
import { BrowseDefinition } from './shared/browse-definition.model';
@@ -111,6 +112,7 @@ export const models =
111112
[
112113
Root,
113114
DSpaceObject,
115+
BibliographyData,
114116
Bundle,
115117
Bitstream,
116118
BitstreamFormat,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
autoserialize,
3+
deserialize,
4+
} from 'cerialize';
5+
import { typedObject } from 'src/app/core/cache/builders/build-decorators';
6+
import { DSpaceObject } from 'src/app/core/shared/dspace-object.model';
7+
import { HALLink } from 'src/app/core/shared/hal-link.model';
8+
import { ResourceType } from 'src/app/core/shared/resource-type';
9+
import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators';
10+
11+
import { Bibliography } from './bibliography.model';
12+
import { BIBLIOGRAPHY } from './bibliography.resource-type';
13+
14+
/**
15+
* Class representing a DSpace Version
16+
*/
17+
@typedObject
18+
export class BibliographyData extends DSpaceObject {
19+
static type = BIBLIOGRAPHY;
20+
21+
/**
22+
* The type for this IdentifierData
23+
*/
24+
@excludeFromEquals
25+
@autoserialize
26+
type: ResourceType;
27+
28+
/**
29+
* The
30+
*/
31+
@autoserialize
32+
bibliographies: Bibliography[];
33+
34+
/**
35+
* The {@link HALLink}s for this IdentifierData
36+
*/
37+
@deserialize
38+
_links: {
39+
self: HALLink;
40+
};
41+
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { autoserialize } from 'cerialize';
2+
3+
export class Bibliography {
4+
@autoserialize
5+
style: string;
6+
7+
@autoserialize
8+
value: string;
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResourceType } from 'src/app/core/shared/resource-type';
2+
3+
/**
4+
* The resource type for Bibliography
5+
*
6+
* Needs to be in a separate file to prevent circular
7+
* dependencies in webpack.
8+
*/
9+
export const BIBLIOGRAPHY = new ResourceType('bibliography');

src/app/core/shared/item.model.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
8282
mappedCollections: HALLink;
8383
relationships: HALLink;
8484
bundles: HALLink;
85+
bibliography: HALLink;
8586
owningCollection: HALLink;
8687
templateItemOf: HALLink;
8788
version: HALLink;
@@ -130,8 +131,8 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject
130131
* The access status for this Item
131132
* Will be undefined unless the access status {@link HALLink} has been resolved.
132133
*/
133-
@link(ACCESS_STATUS, false, 'accessStatus')
134-
accessStatus?: Observable<RemoteData<AccessStatusObject>>;
134+
@link(ACCESS_STATUS, false, 'accessStatus')
135+
accessStatus?: Observable<RemoteData<AccessStatusObject>>;
135136

136137
/**
137138
* The identifier data for this Item

src/app/core/shared/listable.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
44
import { RouterModule } from '@angular/router';
55
import { TranslateModule } from '@ngx-translate/core';
6+
import { ItemPageBibliographyComponent } from 'src/app/item-page/simple/field-components/specific-field/bibliography/item-page-bibliography.component';
67

78
import { CollectionAdminSearchResultGridElementComponent } from '../../admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
89
import { CommunityAdminSearchResultGridElementComponent } from '../../admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
@@ -219,6 +220,7 @@ const ENTRY_COMPONENTS = [
219220
TruncatableComponent,
220221
TruncatablePartComponent,
221222
ThumbnailComponent,
223+
ItemPageBibliographyComponent,
222224
BadgesComponent,
223225
ThemedBadgesComponent,
224226
ItemDetailPreviewComponent,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<a href="javascript:void(0)" (click)="openModal(citationModal)">
2+
<i class="fas fa-quote-right"></i>
3+
{{ "item.cite.open-modal" | translate }}
4+
</a>
5+
6+
<ng-template #citationModal let-modal>
7+
<div class="modal-header">
8+
<h4 class="modal-title">{{ "item.cite.modal.title" | translate }}</h4>
9+
<button
10+
type="button"
11+
class="btn-close"
12+
aria-label="Close"
13+
(click)="modal.dismiss()"
14+
></button>
15+
</div>
16+
17+
<div class="modal-body">
18+
@if (loading) {
19+
<div class="text-center">
20+
<i class="fas fa-spinner fa-spin"></i> {{ "loading" | translate }}
21+
</div>
22+
} @if (error) {
23+
<div class="alert alert-danger">
24+
{{ "item.cite.modal.error" | translate }}
25+
</div>
26+
} @if (bibliographies?.length) { @for (c of bibliographies; track c.style) {
27+
<div class="mb-3">
28+
<strong>{{ c.style | translate }}</strong>
29+
<div>
30+
@if (c.style === 'bibtex') {
31+
<code>{{ c.value }}</code>
32+
} @else {
33+
<span>{{ c.value }}</span>
34+
}
35+
<button
36+
title="{{ 'item.cite.modal.copy' | translate }}"
37+
type="button"
38+
class="btn btn-sm btn-outline-primary mt-1"
39+
(click)="copyToClipboard(c.value)"
40+
>
41+
<i class="fas fa-copy"></i>
42+
</button>
43+
</div>
44+
</div>
45+
} }
46+
</div>
47+
48+
<div class="modal-footer">
49+
<button type="button" class="btn btn-secondary" (click)="modal.close()">
50+
{{ "item.cite.modal.close" | translate }}
51+
</button>
52+
</div>
53+
</ng-template>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
fakeAsync,
5+
TestBed,
6+
tick,
7+
waitForAsync,
8+
} from '@angular/core/testing';
9+
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
10+
import {
11+
TranslateLoader,
12+
TranslateModule,
13+
} from '@ngx-translate/core';
14+
import {
15+
delay,
16+
of,
17+
throwError,
18+
} from 'rxjs';
19+
20+
import { ItemBibliographyService } from '../../../../../core/data/bibliography-data.service';
21+
import { Bibliography } from '../../../../../core/shared/bibliography/bibliography.model';
22+
import { BibliographyData } from '../../../../../core/shared/bibliography/bibliography-data.model';
23+
import { Item } from '../../../../../core/shared/item.model';
24+
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
25+
import { ItemPageBibliographyComponent } from './item-page-bibliography.component';
26+
27+
describe('ItemPageBibliographyComponent', () => {
28+
let component: ItemPageBibliographyComponent;
29+
let fixture: ComponentFixture<ItemPageBibliographyComponent>;
30+
let mockService: jasmine.SpyObj<ItemBibliographyService>;
31+
let mockModalService: jasmine.SpyObj<NgbModal>;
32+
let testItem: Item;
33+
34+
beforeEach(waitForAsync(() => {
35+
mockService = jasmine.createSpyObj('ItemBibliographyService', ['getBibliographies']);
36+
mockModalService = jasmine.createSpyObj('NgbModal', ['open']);
37+
38+
void TestBed.configureTestingModule({
39+
imports: [
40+
ItemPageBibliographyComponent,
41+
TranslateModule.forRoot({
42+
loader: { provide: TranslateLoader, useClass: TranslateLoaderMock },
43+
}),
44+
],
45+
providers: [
46+
{ provide: ItemBibliographyService, useValue: mockService },
47+
{ provide: NgbModal, useValue: mockModalService },
48+
],
49+
schemas: [NO_ERRORS_SCHEMA],
50+
})
51+
.overrideComponent(ItemPageBibliographyComponent, {
52+
set: { providers: [{ provide: NgbModal, useValue: mockModalService }] },
53+
})
54+
.compileComponents();
55+
}));
56+
57+
beforeEach(() => {
58+
fixture = TestBed.createComponent(ItemPageBibliographyComponent);
59+
component = fixture.componentInstance;
60+
testItem = new Item();
61+
component.item = testItem;
62+
fixture.detectChanges();
63+
});
64+
65+
it('should create', () => {
66+
expect(component).toBeTruthy();
67+
});
68+
69+
it('should load bibliographies and open modal', fakeAsync(() => {
70+
const mockBibliographies: Bibliography[] = [
71+
{ style: 'bibtex', value: '@article{test}' },
72+
{ style: 'apa', value: 'Author, 2025' },
73+
];
74+
const mockData = { bibliographies: mockBibliographies };
75+
mockService.getBibliographies.and.returnValue(of(mockData as BibliographyData).pipe(delay(0)));
76+
const modalContent = {};
77+
78+
component.openModal(modalContent);
79+
expect(component.loading).toBeTrue();
80+
expect(component.error).toBeFalse();
81+
82+
tick();
83+
expect(component.bibliographies).toEqual(mockBibliographies);
84+
expect(component.loading).toBeFalse();
85+
expect(mockModalService.open).toHaveBeenCalledWith(modalContent, { size: 'lg' });
86+
}));
87+
88+
it('should handle error when loading bibliographies', fakeAsync(() => {
89+
const mockError = new Error('Failed');
90+
mockService.getBibliographies.and.returnValue(throwError(() => mockError));
91+
const modalContent = {};
92+
93+
component.openModal(modalContent);
94+
tick();
95+
96+
expect(component.loading).toBeFalse();
97+
expect(component.error).toBeTrue();
98+
expect(mockModalService.open).not.toHaveBeenCalled();
99+
}));
100+
101+
it('should copy text to clipboard', () => {
102+
const text = 'Some text to copy';
103+
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
104+
105+
component.copyToClipboard(text);
106+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(text);
107+
});
108+
109+
it('should copy HTML text to clipboard as plain text', () => {
110+
const htmlText = '<p>HTML text</p>';
111+
spyOn(navigator.clipboard, 'writeText').and.returnValue(Promise.resolve());
112+
113+
component.copyToClipboard(htmlText);
114+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('HTML text');
115+
});
116+
});

0 commit comments

Comments
 (0)