mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
Merge pull request #2875 from alexandrevryghem/created-metadata-service-for-metadata-operations_contribute-main
Moved MetadataValue methods to separate service
This commit is contained in:
@@ -29,7 +29,7 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { authReducer } from './core/auth/auth.reducer';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { LocaleService } from './core/locale/locale.service';
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { HeadTagService } from './core/metadata/head-tag.service';
|
||||
import { RouteService } from './core/services/route.service';
|
||||
import {
|
||||
NativeWindowRef,
|
||||
@@ -42,7 +42,7 @@ import { MenuService } from './shared/menu/menu.service';
|
||||
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
||||
import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock';
|
||||
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||
import { MetadataServiceMock } from './shared/mocks/metadata-service.mock';
|
||||
import { HeadTagServiceMock } from './shared/mocks/head-tag-service.mock';
|
||||
import { RouterMock } from './shared/mocks/router.mock';
|
||||
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
||||
@@ -87,7 +87,7 @@ describe('App component', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
||||
{ provide: HeadTagService, useValue: new HeadTagServiceMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { PAGINATED_RELATIONS_TO_ITEMS_OPERATOR } from '../../item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import {
|
||||
@@ -11,7 +15,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
|
||||
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
@@ -20,6 +26,7 @@ import { MetadataRepresentationType } from '../shared/metadata-representation/me
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||
import { FindListOptions } from './find-list-options.model';
|
||||
import { ItemDataService } from './item-data.service';
|
||||
import { buildPaginatedList } from './paginated-list.model';
|
||||
import { RelationshipDataService } from './relationship-data.service';
|
||||
import { DeleteRequest } from './request.models';
|
||||
@@ -123,18 +130,6 @@ describe('RelationshipDataService', () => {
|
||||
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]),
|
||||
});
|
||||
|
||||
function initTestService() {
|
||||
return new RelationshipDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
halService,
|
||||
objectCache as ObjectCacheService,
|
||||
itemService,
|
||||
null,
|
||||
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),
|
||||
);
|
||||
}
|
||||
|
||||
const getRequestEntry$ = (successful: boolean) => {
|
||||
return observableOf({
|
||||
response: { isSuccessful: successful, payload: relationships } as any,
|
||||
@@ -143,11 +138,25 @@ describe('RelationshipDataService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
service = initTestService();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: ItemDataService, useValue: itemService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: PAGINATED_RELATIONS_TO_ITEMS_OPERATOR, useValue: jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v) },
|
||||
{ provide: Store, useValue: provideMockStore() },
|
||||
RelationshipDataService,
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(RelationshipDataService);
|
||||
});
|
||||
|
||||
describe('composition', () => {
|
||||
const initService = () => new RelationshipDataService(null, null, null, null, null, null, null);
|
||||
const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null);
|
||||
|
||||
testSearchDataImplementation(initService);
|
||||
});
|
||||
|
@@ -54,6 +54,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { MetadataService } from '../metadata/metadata.service';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
@@ -128,6 +129,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected halService: HALEndpointService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected metadataService: MetadataService,
|
||||
protected itemService: ItemDataService,
|
||||
protected appStore: Store<AppState>,
|
||||
@Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>,
|
||||
@@ -602,8 +604,8 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
||||
* @param itemType The type of item this metadata value represents (will only be used when no related item can be found, as a fallback)
|
||||
*/
|
||||
resolveMetadataRepresentation(metadatum: MetadataValue, parentItem: DSpaceObject, itemType: string): Observable<MetadataRepresentation> {
|
||||
if (metadatum.isVirtual) {
|
||||
return this.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||
if (this.metadataService.isVirtual(metadatum)) {
|
||||
return this.findById(this.metadataService.virtualValue(metadatum), true, false, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
switchMap((relRD: RemoteData<Relationship>) =>
|
||||
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
|
||||
|
508
src/app/core/metadata/head-tag.service.spec.ts
Normal file
508
src/app/core/metadata/head-tag.service.spec.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing';
|
||||
import {
|
||||
Meta,
|
||||
Title,
|
||||
} from '@angular/platform-browser';
|
||||
import {
|
||||
NavigationEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { createMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AppConfig } from '../../../config/app-config.interface';
|
||||
import {
|
||||
ItemMock,
|
||||
MockBitstream1,
|
||||
MockBitstream2,
|
||||
MockBitstream3,
|
||||
} from '../../shared/mocks/item.mock';
|
||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$,
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { MetadataValue } from '../shared/metadata.models';
|
||||
import { HeadTagService } from './head-tag.service';
|
||||
import {
|
||||
AddMetaTagAction,
|
||||
ClearMetaTagAction,
|
||||
} from './meta-tag.actions';
|
||||
|
||||
describe('HeadTagService', () => {
|
||||
let headTagService: HeadTagService;
|
||||
|
||||
let meta: Meta;
|
||||
|
||||
let title: Title;
|
||||
|
||||
let dsoNameService: DSONameService;
|
||||
|
||||
let bundleDataService;
|
||||
let rootService: RootDataService;
|
||||
let translateService: TranslateService;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
let router: Router;
|
||||
let store;
|
||||
|
||||
let appConfig: AppConfig;
|
||||
|
||||
const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] } } };
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
rootService = jasmine.createSpyObj({
|
||||
findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' }),
|
||||
});
|
||||
bundleDataService = jasmine.createSpyObj({
|
||||
findByItemAndName: mockBundleRD$([MockBitstream3]),
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
meta = jasmine.createSpyObj('meta', {
|
||||
addTag: {},
|
||||
removeTag: {},
|
||||
});
|
||||
title = jasmine.createSpyObj({
|
||||
setTitle: {},
|
||||
});
|
||||
dsoNameService = jasmine.createSpyObj({
|
||||
getName: ItemMock.firstMetadataValue('dc.title'),
|
||||
});
|
||||
router = {
|
||||
url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
events: of(new NavigationEnd(1, '', '')),
|
||||
routerState: {
|
||||
root: {},
|
||||
},
|
||||
} as any as Router;
|
||||
hardRedirectService = jasmine.createSpyObj( {
|
||||
getCurrentOrigin: 'https://request.org',
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
|
||||
store = createMockStore({ initialState });
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
appConfig = {
|
||||
item: {
|
||||
bitstream: {
|
||||
pageSize: 5,
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
headTagService = new HeadTagService(
|
||||
router,
|
||||
translateService,
|
||||
meta,
|
||||
title,
|
||||
dsoNameService,
|
||||
bundleDataService,
|
||||
rootService,
|
||||
store,
|
||||
hardRedirectService,
|
||||
appConfig,
|
||||
authorizationService,
|
||||
);
|
||||
});
|
||||
|
||||
it('items page should set meta tags', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_title',
|
||||
content: 'Test PowerPoint Document',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_publication_date',
|
||||
content: '1650-06-26',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_keywords',
|
||||
content: 'keyword1; keyword2; keyword3',
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Thesis', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_dissertation_name',
|
||||
content: 'Test PowerPoint Document',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
}));
|
||||
|
||||
it('route titles should overwrite dso titles', fakeAsync(() => {
|
||||
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Translated Route Title'));
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
title: 'route.title.key',
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledTimes(2);
|
||||
expect((title.setTitle as jasmine.Spy).calls.argsFor(0)).toEqual(['Test PowerPoint Document']);
|
||||
expect((title.setTitle as jasmine.Spy).calls.argsFor(1)).toEqual(['DSpace :: Translated Route Title']);
|
||||
}));
|
||||
|
||||
it('other navigation should add title and description', fakeAsync(() => {
|
||||
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
title: 'Dummy Title',
|
||||
description: 'This is a dummy item component for testing!',
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'title',
|
||||
content: 'DSpace :: Dummy Title',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'description',
|
||||
content: 'This is a dummy item component for testing!',
|
||||
});
|
||||
}));
|
||||
|
||||
describe(`listenForRouteChange`, () => {
|
||||
it(`should call processRouteChange`, fakeAsync(() => {
|
||||
spyOn(headTagService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
headTagService.listenForRouteChange();
|
||||
tick();
|
||||
expect((headTagService as any).processRouteChange).toHaveBeenCalled();
|
||||
}));
|
||||
it(`should add Generator`, fakeAsync(() => {
|
||||
spyOn(headTagService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
headTagService.listenForRouteChange();
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'Generator',
|
||||
content: 'mock-dspace-version',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_abstract_html_url', () => {
|
||||
it('should use dc.identifier.uri if available', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_abstract_html_url',
|
||||
content: 'https://ddg.gg',
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use current route as fallback', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_abstract_html_url',
|
||||
content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_*_institution / citation_publisher', () => {
|
||||
it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_dissertation_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_publisher for other item types', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_publisher',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_pdf_url', () => {
|
||||
it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
|
||||
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
describe('bitstream not download allowed', () => {
|
||||
it('should not have citation_pdf_url', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' }));
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('no primary Bitstream', () => {
|
||||
it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
describe(`when there's a bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
});
|
||||
|
||||
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download',
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there's no bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
});
|
||||
|
||||
it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download',
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('tagstore', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
(headTagService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should remove previous tags on route change', fakeAsync(() => {
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('name=\'title\'');
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('name=\'description\'');
|
||||
}));
|
||||
|
||||
it('should clear all tags and add new ones on route change', () => {
|
||||
expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]);
|
||||
expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]);
|
||||
expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockType = (mockItem: Item, type: string): Item => {
|
||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[];
|
||||
return typedMockItem;
|
||||
};
|
||||
|
||||
const mockPublisher = (mockItem: Item): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata['dc.publisher'] = [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Mock Publisher',
|
||||
},
|
||||
] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockUri = (mockItem: Item, uri?: string): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable<RemoteData<Bundle>> => {
|
||||
return createSuccessfulRemoteDataObject$(
|
||||
Object.assign(new Bundle(), {
|
||||
name: 'ORIGINAL',
|
||||
bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]),
|
||||
primaryBitstream: createSuccessfulRemoteDataObject$(primary),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList<Bitstream>[] => {
|
||||
return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), {
|
||||
pageInfo: {
|
||||
totalElements: bitstreams.length, // announce multiple elements/pages
|
||||
},
|
||||
_links: index < bitstreams.length - 1
|
||||
? { next: { href: 'not empty' } } // fake link to the next bitstream page
|
||||
: { next: { href: undefined } }, // last page has no link
|
||||
}));
|
||||
};
|
||||
});
|
527
src/app/core/metadata/head-tag.service.ts
Normal file
527
src/app/core/metadata/head-tag.service.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Meta,
|
||||
MetaDefinition,
|
||||
Title,
|
||||
} from '@angular/platform-browser';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
createSelector,
|
||||
select,
|
||||
Store,
|
||||
} from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concat as observableConcat,
|
||||
EMPTY,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../shared/empty.util';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { BundleDataService } from '../data/bundle-data.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { getDownloadableBitstream } from '../shared/bitstream.operators';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import {
|
||||
AddMetaTagAction,
|
||||
ClearMetaTagAction,
|
||||
} from './meta-tag.actions';
|
||||
import { MetaTagState } from './meta-tag.reducer';
|
||||
|
||||
/**
|
||||
* The base selector function to select the metaTag section in the store
|
||||
*/
|
||||
const metaTagSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state.metaTag,
|
||||
);
|
||||
|
||||
/**
|
||||
* Selector function to select the tags in use from the MetaTagState
|
||||
*/
|
||||
const tagsInUseSelector =
|
||||
createSelector(
|
||||
metaTagSelector,
|
||||
(state: MetaTagState) => state.tagsInUse,
|
||||
);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class HeadTagService {
|
||||
|
||||
private currentObject: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
|
||||
|
||||
/**
|
||||
* When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
|
||||
* the first Bitstream to match one of the following MIME types is selected.
|
||||
* See {@linkcode getFirstAllowedFormatBitstreamLink}
|
||||
* @private
|
||||
*/
|
||||
private readonly CITATION_PDF_URL_MIMETYPES = [
|
||||
'application/pdf', // .pdf
|
||||
'application/postscript', // .ps
|
||||
'application/msword', // .doc
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
||||
'application/rtf', // .rtf
|
||||
'application/epub+zip', // .epub
|
||||
];
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
protected translate: TranslateService,
|
||||
protected meta: Meta,
|
||||
protected title: Title,
|
||||
protected dsoNameService: DSONameService,
|
||||
protected bundleDataService: BundleDataService,
|
||||
protected rootService: RootDataService,
|
||||
protected store: Store<CoreState>,
|
||||
protected hardRedirectService: HardRedirectService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
public listenForRouteChange(): void {
|
||||
// This never changes, set it only once
|
||||
this.setGenerator();
|
||||
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.root),
|
||||
map((route: ActivatedRoute) => {
|
||||
route = this.getCurrentRoute(route);
|
||||
return { params: route.params, data: route.data };
|
||||
})).subscribe((routeInfo: any) => {
|
||||
this.processRouteChange(routeInfo);
|
||||
});
|
||||
}
|
||||
|
||||
protected processRouteChange(routeInfo: any): void {
|
||||
this.clearMetaTags();
|
||||
|
||||
if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
|
||||
this.currentObject.next(routeInfo.data.value.dso.payload);
|
||||
this.setDSOMetaTags();
|
||||
}
|
||||
|
||||
if (routeInfo.data.value.title) {
|
||||
const titlePrefix = this.translate.get('repository.title.prefix');
|
||||
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
|
||||
combineLatest([titlePrefix, title]).pipe(take(1)).subscribe(([translatedTitlePrefix, translatedTitle]: [string, string]) => {
|
||||
this.addMetaTag('title', translatedTitlePrefix + translatedTitle);
|
||||
this.title.setTitle(translatedTitlePrefix + translatedTitle);
|
||||
});
|
||||
}
|
||||
if (routeInfo.data.value.description) {
|
||||
this.translate.get(routeInfo.data.value.description).pipe(take(1)).subscribe((translatedDescription: string) => {
|
||||
this.addMetaTag('description', translatedDescription);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
protected setDSOMetaTags(): void {
|
||||
|
||||
this.setTitleTag();
|
||||
this.setDescriptionTag();
|
||||
|
||||
this.setCitationTitleTag();
|
||||
this.setCitationAuthorTags();
|
||||
this.setCitationPublicationDateTag();
|
||||
this.setCitationISSNTag();
|
||||
this.setCitationISBNTag();
|
||||
|
||||
this.setCitationLanguageTag();
|
||||
this.setCitationKeywordsTag();
|
||||
|
||||
this.setCitationAbstractUrlTag();
|
||||
this.setCitationPdfUrlTag();
|
||||
this.setCitationPublisherTag();
|
||||
|
||||
if (this.isDissertation()) {
|
||||
this.setCitationDissertationNameTag();
|
||||
}
|
||||
|
||||
// this.setCitationJournalTitleTag();
|
||||
// this.setCitationVolumeTag();
|
||||
// this.setCitationIssueTag();
|
||||
// this.setCitationFirstPageTag();
|
||||
// this.setCitationLastPageTag();
|
||||
// this.setCitationDOITag();
|
||||
// this.setCitationPMIDTag();
|
||||
|
||||
// this.setCitationFullTextTag();
|
||||
|
||||
// this.setCitationConferenceTag();
|
||||
|
||||
// this.setCitationPatentCountryTag();
|
||||
// this.setCitationPatentNumberTag();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="title" ... > to the <head>
|
||||
*/
|
||||
protected setTitleTag(): void {
|
||||
const value = this.dsoNameService.getName(this.currentObject.getValue());
|
||||
this.addMetaTag('title', value);
|
||||
this.title.setTitle(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="description" ... > to the <head>
|
||||
*/
|
||||
protected setDescriptionTag(): void {
|
||||
// TODO: truncate abstract
|
||||
const value = this.getMetaTagValue('dc.description.abstract');
|
||||
this.addMetaTag('description', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_title" ... > to the <head>
|
||||
*/
|
||||
protected setCitationTitleTag(): void {
|
||||
const value = this.getMetaTagValue('dc.title');
|
||||
this.addMetaTag('citation_title', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_author" ... > to the <head>
|
||||
*/
|
||||
protected setCitationAuthorTags(): void {
|
||||
const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']);
|
||||
this.addMetaTags('citation_author', values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_publication_date" ... > to the <head>
|
||||
*/
|
||||
protected setCitationPublicationDateTag(): void {
|
||||
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
|
||||
this.addMetaTag('citation_publication_date', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_issn" ... > to the <head>
|
||||
*/
|
||||
protected setCitationISSNTag(): void {
|
||||
const value = this.getMetaTagValue('dc.identifier.issn');
|
||||
this.addMetaTag('citation_issn', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_isbn" ... > to the <head>
|
||||
*/
|
||||
protected setCitationISBNTag(): void {
|
||||
const value = this.getMetaTagValue('dc.identifier.isbn');
|
||||
this.addMetaTag('citation_isbn', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_language" ... > to the <head>
|
||||
*/
|
||||
protected setCitationLanguageTag(): void {
|
||||
const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']);
|
||||
this.addMetaTag('citation_language', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_dissertation_name" ... > to the <head>
|
||||
*/
|
||||
protected setCitationDissertationNameTag(): void {
|
||||
const value = this.getMetaTagValue('dc.title');
|
||||
this.addMetaTag('citation_dissertation_name', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dc.publisher to the <head>. The tag name depends on the item type.
|
||||
*/
|
||||
protected setCitationPublisherTag(): void {
|
||||
const value = this.getMetaTagValue('dc.publisher');
|
||||
if (this.isDissertation()) {
|
||||
this.addMetaTag('citation_dissertation_institution', value);
|
||||
} else if (this.isTechReport()) {
|
||||
this.addMetaTag('citation_technical_report_institution', value);
|
||||
} else {
|
||||
this.addMetaTag('citation_publisher', value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_keywords" ... > to the <head>
|
||||
*/
|
||||
protected setCitationKeywordsTag(): void {
|
||||
const value = this.getMetaTagValuesAndCombine('dc.subject');
|
||||
this.addMetaTag('citation_keywords', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_abstract_html_url" ... > to the <head>
|
||||
*/
|
||||
protected setCitationAbstractUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
let url = this.getMetaTagValue('dc.identifier.uri');
|
||||
if (hasNoValue(url)) {
|
||||
url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString();
|
||||
}
|
||||
this.addMetaTag('citation_abstract_html_url', url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_pdf_url" ... > to the <head>
|
||||
*/
|
||||
protected setCitationPdfUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
const item = this.currentObject.value as Item;
|
||||
|
||||
// Retrieve the ORIGINAL bundle for the item
|
||||
this.bundleDataService.findByItemAndName(
|
||||
item,
|
||||
'ORIGINAL',
|
||||
true,
|
||||
true,
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {
|
||||
findListOptions: {
|
||||
// limit the number of bitstreams used to find the citation pdf url to the number
|
||||
// shown by default on an item page
|
||||
elementsPerPage: this.appConfig.item.bitstream.pageSize,
|
||||
},
|
||||
}, followLink('format')),
|
||||
).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((bundle: Bundle) =>
|
||||
// First try the primary bitstream
|
||||
bundle.primaryBitstream.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Bitstream>) => {
|
||||
if (hasValue(rd.payload)) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
// return the bundle as well so we can use it again if there's no primary bitstream
|
||||
map((bitstream: Bitstream) => [bundle, bitstream]),
|
||||
),
|
||||
),
|
||||
switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => {
|
||||
if (hasValue(primaryBitstream)) {
|
||||
// If there was a downloadable primary bitstream, emit its link
|
||||
return [getBitstreamDownloadRoute(primaryBitstream)];
|
||||
} else {
|
||||
// Otherwise consider the regular bitstreams in the bundle
|
||||
return bundle.bitstreams.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((bitstreamRd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) {
|
||||
// If there's only one bitstream in the bundle, emit its link if its downloadable
|
||||
return this.getBitLinkIfDownloadable(bitstreamRd.payload.page[0], bitstreamRd);
|
||||
} else {
|
||||
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
take(1),
|
||||
).subscribe((link: string) => {
|
||||
// Use the found link to set the <meta> tag
|
||||
this.addMetaTag(
|
||||
'citation_pdf_url',
|
||||
new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
return observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
switchMap((bit: Bitstream) => {
|
||||
if (hasValue(bit)) {
|
||||
return [getBitstreamDownloadRoute(bit)];
|
||||
} else {
|
||||
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream
|
||||
* with a MIME type.
|
||||
*
|
||||
* Note this will only check the current page (page size determined item.bitstream.pageSize in the
|
||||
* config) of bitstreams for performance reasons.
|
||||
* See https://github.com/DSpace/DSpace/issues/8648 for more info
|
||||
*
|
||||
* included in {@linkcode CITATION_PDF_URL_MIMETYPES}
|
||||
* @param bitstreamRd
|
||||
* @private
|
||||
*/
|
||||
protected getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) {
|
||||
// Retrieve the formats of all bitstreams in the page sequentially
|
||||
return observableConcat(
|
||||
...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||
// for the link at the end
|
||||
map((format: BitstreamFormat) => [bitstream, format]),
|
||||
)),
|
||||
).pipe(
|
||||
// Verify that the bitstream is downloadable
|
||||
mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
map((bit: Bitstream) => [bit, format]),
|
||||
)),
|
||||
// Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check
|
||||
filter(([bitstream, format]: [Bitstream, BitstreamFormat]) =>
|
||||
hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||
// We only need 1
|
||||
take(1),
|
||||
// Emit the link of the match
|
||||
// tap((v) => console.log('result', v)),
|
||||
map(([bitstream, format]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)),
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="Generator" ... > to the <head> containing the current DSpace version
|
||||
*/
|
||||
protected setGenerator(): void {
|
||||
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
|
||||
this.meta.addTag({ name: 'Generator', content: root.dspaceVersion });
|
||||
});
|
||||
}
|
||||
|
||||
protected hasType(value: string): boolean {
|
||||
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this._item is a dissertation
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if this._item has a dc.type equal to 'Thesis'
|
||||
*/
|
||||
protected isDissertation(): boolean {
|
||||
return this.hasType('thesis');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this._item is a technical report
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if this._item has a dc.type equal to 'Technical Report'
|
||||
*/
|
||||
protected isTechReport(): boolean {
|
||||
return this.hasType('technical report');
|
||||
}
|
||||
|
||||
protected getMetaTagValue(key: string): string {
|
||||
return this.currentObject.value.firstMetadataValue(key);
|
||||
}
|
||||
|
||||
protected getFirstMetaTagValue(keys: string[]): string {
|
||||
return this.currentObject.value.firstMetadataValue(keys);
|
||||
}
|
||||
|
||||
protected getMetaTagValuesAndCombine(key: string): string {
|
||||
return this.getMetaTagValues([key]).join('; ');
|
||||
}
|
||||
|
||||
protected getMetaTagValues(keys: string[]): string[] {
|
||||
return this.currentObject.value.allMetadataValues(keys);
|
||||
}
|
||||
|
||||
protected addMetaTag(name: string, content: string): void {
|
||||
if (content) {
|
||||
const tag = { name, content } as MetaDefinition;
|
||||
this.meta.addTag(tag);
|
||||
this.storeTag(name);
|
||||
}
|
||||
}
|
||||
|
||||
protected addMetaTags(name: string, content: string[]): void {
|
||||
for (const value of content) {
|
||||
this.addMetaTag(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
protected storeTag(key: string): void {
|
||||
this.store.dispatch(new AddMetaTagAction(key));
|
||||
}
|
||||
|
||||
protected clearMetaTags(): void {
|
||||
this.store.pipe(
|
||||
select(tagsInUseSelector),
|
||||
take(1),
|
||||
).subscribe((tagsInUse: string[]) => {
|
||||
for (const name of tagsInUse) {
|
||||
this.meta.removeTag('name=\'' + name + '\'');
|
||||
}
|
||||
this.store.dispatch(new ClearMetaTagAction());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,520 +1,16 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing';
|
||||
import {
|
||||
Meta,
|
||||
Title,
|
||||
} from '@angular/platform-browser';
|
||||
import {
|
||||
NavigationEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { createMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppConfig } from '../../../config/app-config.interface';
|
||||
import {
|
||||
ItemMock,
|
||||
MockBitstream1,
|
||||
MockBitstream2,
|
||||
MockBitstream3,
|
||||
} from '../../shared/mocks/item.mock';
|
||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$,
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { MetadataValue } from '../shared/metadata.models';
|
||||
import {
|
||||
AddMetaTagAction,
|
||||
ClearMetaTagAction,
|
||||
} from './meta-tag.actions';
|
||||
import { MetadataService } from './metadata.service';
|
||||
|
||||
describe('MetadataService', () => {
|
||||
let metadataService: MetadataService;
|
||||
|
||||
let meta: Meta;
|
||||
|
||||
let title: Title;
|
||||
|
||||
let dsoNameService: DSONameService;
|
||||
|
||||
let bundleDataService;
|
||||
let bitstreamDataService;
|
||||
let rootService: RootDataService;
|
||||
let translateService: TranslateService;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
let router: Router;
|
||||
let store;
|
||||
|
||||
let appConfig: AppConfig;
|
||||
|
||||
const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] } } };
|
||||
|
||||
let service: MetadataService;
|
||||
|
||||
beforeEach(() => {
|
||||
rootService = jasmine.createSpyObj({
|
||||
findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' }),
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj({
|
||||
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3])),
|
||||
});
|
||||
bundleDataService = jasmine.createSpyObj({
|
||||
findByItemAndName: mockBundleRD$([MockBitstream3]),
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
meta = jasmine.createSpyObj('meta', {
|
||||
addTag: {},
|
||||
removeTag: {},
|
||||
});
|
||||
title = jasmine.createSpyObj({
|
||||
setTitle: {},
|
||||
});
|
||||
dsoNameService = jasmine.createSpyObj({
|
||||
getName: ItemMock.firstMetadataValue('dc.title'),
|
||||
});
|
||||
router = {
|
||||
url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
events: of(new NavigationEnd(1, '', '')),
|
||||
routerState: {
|
||||
root: {},
|
||||
},
|
||||
} as any as Router;
|
||||
hardRedirectService = jasmine.createSpyObj( {
|
||||
getCurrentOrigin: 'https://request.org',
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
});
|
||||
|
||||
store = createMockStore({ initialState });
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
appConfig = {
|
||||
item: {
|
||||
bitstream: {
|
||||
pageSize: 5,
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
metadataService = new MetadataService(
|
||||
router,
|
||||
translateService,
|
||||
meta,
|
||||
title,
|
||||
dsoNameService,
|
||||
bundleDataService,
|
||||
bitstreamDataService,
|
||||
undefined,
|
||||
rootService,
|
||||
store,
|
||||
hardRedirectService,
|
||||
appConfig,
|
||||
authorizationService,
|
||||
);
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(MetadataService);
|
||||
});
|
||||
|
||||
it('items page should set meta tags', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_title',
|
||||
content: 'Test PowerPoint Document',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_publication_date',
|
||||
content: '1650-06-26',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_keywords',
|
||||
content: 'keyword1; keyword2; keyword3',
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Thesis', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_dissertation_name',
|
||||
content: 'Test PowerPoint Document',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
}));
|
||||
|
||||
it('route titles should overwrite dso titles', fakeAsync(() => {
|
||||
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Translated Route Title'));
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
title: 'route.title.key',
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledTimes(2);
|
||||
expect((title.setTitle as jasmine.Spy).calls.argsFor(0)).toEqual(['Test PowerPoint Document']);
|
||||
expect((title.setTitle as jasmine.Spy).calls.argsFor(1)).toEqual(['DSpace :: Translated Route Title']);
|
||||
}));
|
||||
|
||||
it('other navigation should add title and description', fakeAsync(() => {
|
||||
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
title: 'Dummy Title',
|
||||
description: 'This is a dummy item component for testing!',
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'title',
|
||||
content: 'DSpace :: Dummy Title',
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'description',
|
||||
content: 'This is a dummy item component for testing!',
|
||||
});
|
||||
}));
|
||||
|
||||
describe(`listenForRouteChange`, () => {
|
||||
it(`should call processRouteChange`, fakeAsync(() => {
|
||||
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
metadataService.listenForRouteChange();
|
||||
tick();
|
||||
expect((metadataService as any).processRouteChange).toHaveBeenCalled();
|
||||
}));
|
||||
it(`should add Generator`, fakeAsync(() => {
|
||||
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
metadataService.listenForRouteChange();
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'Generator',
|
||||
content: 'mock-dspace-version',
|
||||
});
|
||||
}));
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('citation_abstract_html_url', () => {
|
||||
it('should use dc.identifier.uri if available', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_abstract_html_url',
|
||||
content: 'https://ddg.gg',
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use current route as fallback', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_abstract_html_url',
|
||||
content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_*_institution / citation_publisher', () => {
|
||||
it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_dissertation_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_publisher for other item types', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_publisher',
|
||||
content: 'Mock Publisher',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_pdf_url', () => {
|
||||
it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
describe('bitstream not download allowed', () => {
|
||||
it('should not have citation_pdf_url', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' }));
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('no primary Bitstream', () => {
|
||||
it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download',
|
||||
});
|
||||
}));
|
||||
|
||||
describe(`when there's a bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
});
|
||||
|
||||
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download',
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when there's no bitstream with an allowed format on the first page`, () => {
|
||||
let bitstreams;
|
||||
|
||||
beforeEach(() => {
|
||||
bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
});
|
||||
|
||||
it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith({
|
||||
name: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download',
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('tagstore', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
},
|
||||
},
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should remove previous tags on route change', fakeAsync(() => {
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('name=\'title\'');
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('name=\'description\'');
|
||||
}));
|
||||
|
||||
it('should clear all tags and add new ones on route change', () => {
|
||||
expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]);
|
||||
expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]);
|
||||
expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockType = (mockItem: Item, type: string): Item => {
|
||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[];
|
||||
return typedMockItem;
|
||||
};
|
||||
|
||||
const mockPublisher = (mockItem: Item): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata['dc.publisher'] = [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Mock Publisher',
|
||||
},
|
||||
] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockUri = (mockItem: Item, uri?: string): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable<RemoteData<Bundle>> => {
|
||||
return createSuccessfulRemoteDataObject$(
|
||||
Object.assign(new Bundle(), {
|
||||
name: 'ORIGINAL',
|
||||
bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]),
|
||||
primaryBitstream: createSuccessfulRemoteDataObject$(primary),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList<Bitstream>[] => {
|
||||
return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), {
|
||||
pageInfo: {
|
||||
totalElements: bitstreams.length, // announce multiple elements/pages
|
||||
},
|
||||
_links: index < bitstreams.length - 1
|
||||
? { next: { href: 'not empty' } } // fake link to the next bitstream page
|
||||
: { next: { href: undefined } }, // last page has no link
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
@@ -1,529 +1,37 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
Meta,
|
||||
MetaDefinition,
|
||||
Title,
|
||||
} from '@angular/platform-browser';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
NavigationEnd,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import {
|
||||
createSelector,
|
||||
select,
|
||||
Store,
|
||||
} from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concat as observableConcat,
|
||||
EMPTY,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../../config/app-config.interface';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../../shared/empty.util';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||
import { BundleDataService } from '../data/bundle-data.service';
|
||||
import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { getDownloadableBitstream } from '../shared/bitstream.operators';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import {
|
||||
AddMetaTagAction,
|
||||
ClearMetaTagAction,
|
||||
} from './meta-tag.actions';
|
||||
import { MetaTagState } from './meta-tag.reducer';
|
||||
MetadataValue,
|
||||
VIRTUAL_METADATA_PREFIX,
|
||||
} from '../shared/metadata.models';
|
||||
|
||||
/**
|
||||
* The base selector function to select the metaTag section in the store
|
||||
* Service for working with DSpace object metadata.
|
||||
*/
|
||||
const metaTagSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state.metaTag,
|
||||
);
|
||||
|
||||
/**
|
||||
* Selector function to select the tags in use from the MetaTagState
|
||||
*/
|
||||
const tagsInUseSelector =
|
||||
createSelector(
|
||||
metaTagSelector,
|
||||
(state: MetaTagState) => state.tagsInUse,
|
||||
);
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MetadataService {
|
||||
|
||||
private currentObject: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
|
||||
|
||||
/**
|
||||
* When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
|
||||
* the first Bitstream to match one of the following MIME types is selected.
|
||||
* See {@linkcode getFirstAllowedFormatBitstreamLink}
|
||||
* @private
|
||||
* Returns true if this Metadata authority key starts with 'virtual::'
|
||||
*/
|
||||
private readonly CITATION_PDF_URL_MIMETYPES = [
|
||||
'application/pdf', // .pdf
|
||||
'application/postscript', // .ps
|
||||
'application/msword', // .doc
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
||||
'application/rtf', // .rtf
|
||||
'application/epub+zip', // .epub
|
||||
];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private translate: TranslateService,
|
||||
private meta: Meta,
|
||||
private title: Title,
|
||||
private dsoNameService: DSONameService,
|
||||
private bundleDataService: BundleDataService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||
private rootService: RootDataService,
|
||||
private store: Store<CoreState>,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
public listenForRouteChange(): void {
|
||||
// This never changes, set it only once
|
||||
this.setGenerator();
|
||||
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.root),
|
||||
map((route: ActivatedRoute) => {
|
||||
route = this.getCurrentRoute(route);
|
||||
return { params: route.params, data: route.data };
|
||||
})).subscribe((routeInfo: any) => {
|
||||
this.processRouteChange(routeInfo);
|
||||
});
|
||||
}
|
||||
|
||||
private processRouteChange(routeInfo: any): void {
|
||||
this.clearMetaTags();
|
||||
|
||||
if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
|
||||
this.currentObject.next(routeInfo.data.value.dso.payload);
|
||||
this.setDSOMetaTags();
|
||||
}
|
||||
|
||||
if (routeInfo.data.value.title) {
|
||||
const titlePrefix = this.translate.get('repository.title.prefix');
|
||||
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
|
||||
combineLatest([titlePrefix, title]).pipe(take(1)).subscribe(([translatedTitlePrefix, translatedTitle]: [string, string]) => {
|
||||
this.addMetaTag('title', translatedTitlePrefix + translatedTitle);
|
||||
this.title.setTitle(translatedTitlePrefix + translatedTitle);
|
||||
});
|
||||
}
|
||||
if (routeInfo.data.value.description) {
|
||||
this.translate.get(routeInfo.data.value.description).pipe(take(1)).subscribe((translatedDescription: string) => {
|
||||
this.addMetaTag('description', translatedDescription);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
private setDSOMetaTags(): void {
|
||||
|
||||
this.setTitleTag();
|
||||
this.setDescriptionTag();
|
||||
|
||||
this.setCitationTitleTag();
|
||||
this.setCitationAuthorTags();
|
||||
this.setCitationPublicationDateTag();
|
||||
this.setCitationISSNTag();
|
||||
this.setCitationISBNTag();
|
||||
|
||||
this.setCitationLanguageTag();
|
||||
this.setCitationKeywordsTag();
|
||||
|
||||
this.setCitationAbstractUrlTag();
|
||||
this.setCitationPdfUrlTag();
|
||||
this.setCitationPublisherTag();
|
||||
|
||||
if (this.isDissertation()) {
|
||||
this.setCitationDissertationNameTag();
|
||||
}
|
||||
|
||||
// this.setCitationJournalTitleTag();
|
||||
// this.setCitationVolumeTag();
|
||||
// this.setCitationIssueTag();
|
||||
// this.setCitationFirstPageTag();
|
||||
// this.setCitationLastPageTag();
|
||||
// this.setCitationDOITag();
|
||||
// this.setCitationPMIDTag();
|
||||
|
||||
// this.setCitationFullTextTag();
|
||||
|
||||
// this.setCitationConferenceTag();
|
||||
|
||||
// this.setCitationPatentCountryTag();
|
||||
// this.setCitationPatentNumberTag();
|
||||
|
||||
public isVirtual(metadataValue: MetadataValue | undefined): boolean {
|
||||
return hasValue(metadataValue?.authority) && metadataValue.authority.startsWith(VIRTUAL_METADATA_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="title" ... > to the <head>
|
||||
* If this is a virtual Metadata, it returns everything in the authority key after 'virtual::'.
|
||||
*
|
||||
* Returns undefined otherwise.
|
||||
*/
|
||||
private setTitleTag(): void {
|
||||
const value = this.dsoNameService.getName(this.currentObject.getValue());
|
||||
this.addMetaTag('title', value);
|
||||
this.title.setTitle(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="description" ... > to the <head>
|
||||
*/
|
||||
private setDescriptionTag(): void {
|
||||
// TODO: truncate abstract
|
||||
const value = this.getMetaTagValue('dc.description.abstract');
|
||||
this.addMetaTag('description', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_title" ... > to the <head>
|
||||
*/
|
||||
private setCitationTitleTag(): void {
|
||||
const value = this.getMetaTagValue('dc.title');
|
||||
this.addMetaTag('citation_title', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_author" ... > to the <head>
|
||||
*/
|
||||
private setCitationAuthorTags(): void {
|
||||
const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']);
|
||||
this.addMetaTags('citation_author', values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_publication_date" ... > to the <head>
|
||||
*/
|
||||
private setCitationPublicationDateTag(): void {
|
||||
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
|
||||
this.addMetaTag('citation_publication_date', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_issn" ... > to the <head>
|
||||
*/
|
||||
private setCitationISSNTag(): void {
|
||||
const value = this.getMetaTagValue('dc.identifier.issn');
|
||||
this.addMetaTag('citation_issn', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_isbn" ... > to the <head>
|
||||
*/
|
||||
private setCitationISBNTag(): void {
|
||||
const value = this.getMetaTagValue('dc.identifier.isbn');
|
||||
this.addMetaTag('citation_isbn', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_language" ... > to the <head>
|
||||
*/
|
||||
private setCitationLanguageTag(): void {
|
||||
const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']);
|
||||
this.addMetaTag('citation_language', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_dissertation_name" ... > to the <head>
|
||||
*/
|
||||
private setCitationDissertationNameTag(): void {
|
||||
const value = this.getMetaTagValue('dc.title');
|
||||
this.addMetaTag('citation_dissertation_name', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dc.publisher to the <head>. The tag name depends on the item type.
|
||||
*/
|
||||
private setCitationPublisherTag(): void {
|
||||
const value = this.getMetaTagValue('dc.publisher');
|
||||
if (this.isDissertation()) {
|
||||
this.addMetaTag('citation_dissertation_institution', value);
|
||||
} else if (this.isTechReport()) {
|
||||
this.addMetaTag('citation_technical_report_institution', value);
|
||||
public virtualValue(metadataValue: MetadataValue | undefined): string {
|
||||
if (this.isVirtual) {
|
||||
return metadataValue.authority.substring(metadataValue.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length);
|
||||
} else {
|
||||
this.addMetaTag('citation_publisher', value);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_keywords" ... > to the <head>
|
||||
*/
|
||||
private setCitationKeywordsTag(): void {
|
||||
const value = this.getMetaTagValuesAndCombine('dc.subject');
|
||||
this.addMetaTag('citation_keywords', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_abstract_html_url" ... > to the <head>
|
||||
*/
|
||||
private setCitationAbstractUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
let url = this.getMetaTagValue('dc.identifier.uri');
|
||||
if (hasNoValue(url)) {
|
||||
url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString();
|
||||
}
|
||||
this.addMetaTag('citation_abstract_html_url', url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_pdf_url" ... > to the <head>
|
||||
*/
|
||||
private setCitationPdfUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
const item = this.currentObject.value as Item;
|
||||
|
||||
// Retrieve the ORIGINAL bundle for the item
|
||||
this.bundleDataService.findByItemAndName(
|
||||
item,
|
||||
'ORIGINAL',
|
||||
true,
|
||||
true,
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {
|
||||
findListOptions: {
|
||||
// limit the number of bitstreams used to find the citation pdf url to the number
|
||||
// shown by default on an item page
|
||||
elementsPerPage: this.appConfig.item.bitstream.pageSize,
|
||||
},
|
||||
}, followLink('format')),
|
||||
).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((bundle: Bundle) =>
|
||||
// First try the primary bitstream
|
||||
bundle.primaryBitstream.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Bitstream>) => {
|
||||
if (hasValue(rd.payload)) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
// return the bundle as well so we can use it again if there's no primary bitstream
|
||||
map((bitstream: Bitstream) => [bundle, bitstream]),
|
||||
),
|
||||
),
|
||||
switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => {
|
||||
if (hasValue(primaryBitstream)) {
|
||||
// If there was a downloadable primary bitstream, emit its link
|
||||
return [getBitstreamDownloadRoute(primaryBitstream)];
|
||||
} else {
|
||||
// Otherwise consider the regular bitstreams in the bundle
|
||||
return bundle.bitstreams.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((bitstreamRd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) {
|
||||
// If there's only one bitstream in the bundle, emit its link if its downloadable
|
||||
return this.getBitLinkIfDownloadable(bitstreamRd.payload.page[0], bitstreamRd);
|
||||
} else {
|
||||
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
take(1),
|
||||
).subscribe((link: string) => {
|
||||
// Use the found link to set the <meta> tag
|
||||
this.addMetaTag(
|
||||
'citation_pdf_url',
|
||||
new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
return observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
switchMap((bit: Bitstream) => {
|
||||
if (hasValue(bit)) {
|
||||
return [getBitstreamDownloadRoute(bit)];
|
||||
} else {
|
||||
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream
|
||||
* with a MIME type.
|
||||
*
|
||||
* Note this will only check the current page (page size determined item.bitstream.pageSize in the
|
||||
* config) of bitstreams for performance reasons.
|
||||
* See https://github.com/DSpace/DSpace/issues/8648 for more info
|
||||
*
|
||||
* included in {@linkcode CITATION_PDF_URL_MIMETYPES}
|
||||
* @param bitstreamRd
|
||||
* @private
|
||||
*/
|
||||
private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) {
|
||||
// Retrieve the formats of all bitstreams in the page sequentially
|
||||
return observableConcat(
|
||||
...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||
// for the link at the end
|
||||
map((format: BitstreamFormat) => [bitstream, format]),
|
||||
)),
|
||||
).pipe(
|
||||
// Verify that the bitstream is downloadable
|
||||
mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe(
|
||||
getDownloadableBitstream(this.authorizationService),
|
||||
map((bit: Bitstream) => [bit, format]),
|
||||
)),
|
||||
// Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check
|
||||
filter(([bitstream, format]: [Bitstream, BitstreamFormat]) =>
|
||||
hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||
// We only need 1
|
||||
take(1),
|
||||
// Emit the link of the match
|
||||
// tap((v) => console.log('result', v)),
|
||||
map(([bitstream ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)),
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="Generator" ... > to the <head> containing the current DSpace version
|
||||
*/
|
||||
private setGenerator(): void {
|
||||
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
|
||||
this.meta.addTag({ name: 'Generator', content: root.dspaceVersion });
|
||||
});
|
||||
}
|
||||
|
||||
private hasType(value: string): boolean {
|
||||
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this._item is a dissertation
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if this._item has a dc.type equal to 'Thesis'
|
||||
*/
|
||||
private isDissertation(): boolean {
|
||||
return this.hasType('thesis');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this._item is a technical report
|
||||
*
|
||||
* @returns {boolean}
|
||||
* true if this._item has a dc.type equal to 'Technical Report'
|
||||
*/
|
||||
private isTechReport(): boolean {
|
||||
return this.hasType('technical report');
|
||||
}
|
||||
|
||||
private getMetaTagValue(key: string): string {
|
||||
return this.currentObject.value.firstMetadataValue(key);
|
||||
}
|
||||
|
||||
private getFirstMetaTagValue(keys: string[]): string {
|
||||
return this.currentObject.value.firstMetadataValue(keys);
|
||||
}
|
||||
|
||||
private getMetaTagValuesAndCombine(key: string): string {
|
||||
return this.getMetaTagValues([key]).join('; ');
|
||||
}
|
||||
|
||||
private getMetaTagValues(keys: string[]): string[] {
|
||||
return this.currentObject.value.allMetadataValues(keys);
|
||||
}
|
||||
|
||||
private addMetaTag(name: string, content: string): void {
|
||||
if (content) {
|
||||
const tag = { name, content } as MetaDefinition;
|
||||
this.meta.addTag(tag);
|
||||
this.storeTag(name);
|
||||
}
|
||||
}
|
||||
|
||||
private addMetaTags(name: string, content: string[]): void {
|
||||
for (const value of content) {
|
||||
this.addMetaTag(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private storeTag(key: string): void {
|
||||
this.store.dispatch(new AddMetaTagAction(key));
|
||||
}
|
||||
|
||||
public clearMetaTags() {
|
||||
this.store.pipe(
|
||||
select(tagsInUseSelector),
|
||||
take(1),
|
||||
).subscribe((tagsInUse: string[]) => {
|
||||
for (const name of tagsInUse) {
|
||||
this.meta.removeTag('name=\'' + name + '\'');
|
||||
}
|
||||
this.store.dispatch(new ClearMetaTagAction());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -6,8 +6,6 @@ import {
|
||||
} from 'cerialize';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
export const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
||||
|
||||
/** A single metadata value and its properties. */
|
||||
@@ -58,24 +56,6 @@ export class MetadataValue implements MetadataValueInterface {
|
||||
@autoserialize
|
||||
confidence: number;
|
||||
|
||||
/**
|
||||
* Returns true if this Metadatum's authority key starts with 'virtual::'
|
||||
*/
|
||||
get isVirtual(): boolean {
|
||||
return hasValue(this.authority) && this.authority.startsWith(VIRTUAL_METADATA_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is a virtual Metadatum, it returns everything in the authority key after 'virtual::'.
|
||||
* Returns undefined otherwise.
|
||||
*/
|
||||
get virtualValue(): string {
|
||||
if (this.isVirtual) {
|
||||
return this.authority.substring(this.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Constraints for matching metadata values. */
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="d-flex flex-row ds-value-row" *ngVar="mdValue.newValue.isVirtual as isVirtual" role="row"
|
||||
<div class="d-flex flex-row ds-value-row" *ngVar="metadataService.isVirtual(mdValue.newValue) as isVirtual" role="row"
|
||||
cdkDrag (cdkDragStarted)="dragging.emit(true)" (cdkDragEnded)="dragging.emit(false)"
|
||||
[ngClass]="{ 'ds-warning': mdValue.reordered || mdValue.change === DsoEditMetadataChangeTypeEnum.UPDATE, 'ds-danger': mdValue.change === DsoEditMetadataChangeTypeEnum.REMOVE, 'ds-success': mdValue.change === DsoEditMetadataChangeTypeEnum.ADD, 'h-100': isOnlyValue }">
|
||||
<div class="flex-grow-1 ds-flex-cell ds-value-cell d-flex flex-column" *ngVar="(mdRepresentation$ | async) as mdRepresentation" role="cell">
|
||||
|
@@ -45,6 +45,7 @@ import { NotificationsService } from 'src/app/shared/notifications/notifications
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||
import { MetadataService } from '../../../core/metadata/metadata.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
@@ -212,14 +213,17 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
||||
private isScrollableVocabulary$: Observable<boolean>;
|
||||
private isSuggesterVocabulary$: Observable<boolean>;
|
||||
|
||||
constructor(protected relationshipService: RelationshipDataService,
|
||||
protected dsoNameService: DSONameService,
|
||||
protected vocabularyService: VocabularyService,
|
||||
protected itemService: ItemDataService,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected registryService: RegistryService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService) {
|
||||
constructor(
|
||||
protected relationshipService: RelationshipDataService,
|
||||
protected dsoNameService: DSONameService,
|
||||
protected vocabularyService: VocabularyService,
|
||||
protected itemService: ItemDataService,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected registryService: RegistryService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected metadataService: MetadataService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -231,7 +235,7 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
||||
* Initialise potential properties of a virtual metadata value
|
||||
*/
|
||||
initVirtualProperties(): void {
|
||||
this.mdRepresentation$ = this.mdValue.newValue.isVirtual ?
|
||||
this.mdRepresentation$ = this.metadataService.isVirtual(this.mdValue.newValue) ?
|
||||
this.relationshipService.resolveMetadataRepresentation(this.mdValue.newValue, this.dso, 'Item')
|
||||
.pipe(
|
||||
map((mdRepresentation: MetadataRepresentation) =>
|
||||
|
@@ -31,7 +31,6 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { authReducer } from './core/auth/auth.reducer';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { LocaleService } from './core/locale/locale.service';
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { RouteService } from './core/services/route.service';
|
||||
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||
import { InitService } from './init.service';
|
||||
@@ -49,6 +48,9 @@ import createSpyObj = jasmine.createSpyObj;
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
import { HeadTagService } from './core/metadata/head-tag.service';
|
||||
import { HeadTagServiceMock } from './shared/mocks/head-tag-service.mock';
|
||||
|
||||
let spy: SpyObj<any>;
|
||||
|
||||
@Injectable()
|
||||
@@ -138,7 +140,7 @@ describe('InitService', () => {
|
||||
let correlationIdServiceSpy;
|
||||
let dspaceTransferStateSpy;
|
||||
let transferStateSpy;
|
||||
let metadataServiceSpy;
|
||||
let headTagService: HeadTagServiceMock;
|
||||
let breadcrumbsServiceSpy;
|
||||
let menuServiceSpy;
|
||||
|
||||
@@ -164,9 +166,7 @@ describe('InitService', () => {
|
||||
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
||||
'listenForRouteChanges',
|
||||
]);
|
||||
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
|
||||
'listenForRouteChange',
|
||||
]);
|
||||
headTagService = new HeadTagServiceMock();
|
||||
menuServiceSpy = jasmine.createSpyObj('menuServiceSpy', [
|
||||
'listenForRouteChanges',
|
||||
]);
|
||||
@@ -190,7 +190,7 @@ describe('InitService', () => {
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: MetadataService, useValue: metadataServiceSpy },
|
||||
{ provide: HeadTagService, useValue: headTagService },
|
||||
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
@@ -206,9 +206,9 @@ describe('InitService', () => {
|
||||
|
||||
describe('initRouteListeners', () => {
|
||||
it('should call listenForRouteChanges', inject([InitService], (service) => {
|
||||
// @ts-ignore
|
||||
spyOn(headTagService, 'listenForRouteChange');
|
||||
service.initRouteListeners();
|
||||
expect(metadataServiceSpy.listenForRouteChange).toHaveBeenCalledTimes(1);
|
||||
expect(headTagService.listenForRouteChange).toHaveBeenCalledTimes(1);
|
||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
@@ -38,7 +38,7 @@ import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||
import { LAZY_DATA_SERVICES } from './core/data-services-map';
|
||||
import { LocaleService } from './core/locale/locale.service';
|
||||
import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { HeadTagService } from './core/metadata/head-tag.service';
|
||||
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||
import { dsDynamicFormControlMapFn } from './shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
@@ -70,7 +70,7 @@ export abstract class InitService {
|
||||
protected translate: TranslateService,
|
||||
protected localeService: LocaleService,
|
||||
protected angulartics2DSpace: Angulartics2DSpace,
|
||||
protected metadata: MetadataService,
|
||||
protected headTagService: HeadTagService,
|
||||
protected breadcrumbsService: BreadcrumbsService,
|
||||
protected themeService: ThemeService,
|
||||
protected menuService: MenuService,
|
||||
@@ -207,13 +207,13 @@ export abstract class InitService {
|
||||
|
||||
/**
|
||||
* Start route-listening subscriptions
|
||||
* - {@link MetadataService.listenForRouteChange}
|
||||
* - {@link HeadTagService.listenForRouteChange}
|
||||
* - {@link BreadcrumbsService.listenForRouteChanges}
|
||||
* - {@link ThemeService.listenForRouteChanges}
|
||||
* @protected
|
||||
*/
|
||||
protected initRouteListeners(): void {
|
||||
this.metadata.listenForRouteChange();
|
||||
this.headTagService.listenForRouteChange();
|
||||
this.breadcrumbsService.listenForRouteChanges();
|
||||
this.themeService.listenForRouteChanges();
|
||||
this.menuService.listenForRouteChanges();
|
||||
|
@@ -28,12 +28,13 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
import { HeadTagService } from '../../core/metadata/head-tag.service';
|
||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { HeadTagServiceMock } from '../../shared/mocks/head-tag-service.mock';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import {
|
||||
@@ -74,13 +75,6 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), {
|
||||
isWithdrawn: true,
|
||||
});
|
||||
|
||||
const metadataServiceStub = {
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
processRemoteData: () => {
|
||||
},
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
};
|
||||
|
||||
describe('FullItemPageComponent', () => {
|
||||
let comp: FullItemPageComponent;
|
||||
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||
@@ -93,6 +87,7 @@ describe('FullItemPageComponent', () => {
|
||||
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
|
||||
let headTagService: HeadTagServiceMock;
|
||||
|
||||
const mocklink = {
|
||||
href: 'http://test.org',
|
||||
@@ -143,6 +138,8 @@ describe('FullItemPageComponent', () => {
|
||||
getInboxRelationLink: observableOf('http://test.org'),
|
||||
});
|
||||
|
||||
headTagService = new HeadTagServiceMock();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -153,7 +150,7 @@ describe('FullItemPageComponent', () => {
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: MetadataService, useValue: metadataServiceStub },
|
||||
{ provide: HeadTagService, useValue: headTagService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||
|
@@ -26,7 +26,6 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
import {
|
||||
LinkDefinition,
|
||||
LinkHeadService,
|
||||
@@ -92,12 +91,6 @@ describe('ItemPageComponent', () => {
|
||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
|
||||
|
||||
const mockMetadataService = {
|
||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||
processRemoteData: () => {
|
||||
},
|
||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||
};
|
||||
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
||||
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }),
|
||||
});
|
||||
@@ -141,7 +134,6 @@ describe('ItemPageComponent', () => {
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: MetadataService, useValue: mockMetadataService },
|
||||
{ provide: Router, useValue: {} },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||
|
@@ -17,6 +17,7 @@ import { map } from 'rxjs/operators';
|
||||
import { BrowseService } from '../../../core/browse/browse.service';
|
||||
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
|
||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||
import { MetadataService } from '../../../core/metadata/metadata.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
|
||||
@@ -76,6 +77,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
||||
constructor(
|
||||
public relationshipService: RelationshipDataService,
|
||||
protected browseDefinitionDataService: BrowseDefinitionDataService,
|
||||
protected metadataService: MetadataService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -101,7 +103,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
||||
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
|
||||
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
||||
.map((metadatum: MetadataValue) => {
|
||||
if (metadatum.isVirtual) {
|
||||
if (this.metadataService.isVirtual(metadatum)) {
|
||||
return this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType);
|
||||
} else {
|
||||
// Check for a configured browse link and return a standard metadata representation
|
||||
|
@@ -5,46 +5,22 @@ import {
|
||||
TestBed,
|
||||
} from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import {
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
} from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component';
|
||||
import { storeModuleConfig } from '../app.reducer';
|
||||
import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component';
|
||||
import { authReducer } from '../core/auth/auth.reducer';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { LocaleService } from '../core/locale/locale.service';
|
||||
import { MetadataService } from '../core/metadata/metadata.service';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import {
|
||||
NativeWindowRef,
|
||||
NativeWindowService,
|
||||
} from '../core/services/window.service';
|
||||
import { ThemedFooterComponent } from '../footer/themed-footer.component';
|
||||
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MockActivatedRoute } from '../shared/mocks/active-router.mock';
|
||||
import { AngularticsProviderMock } from '../shared/mocks/angulartics-provider.service.mock';
|
||||
import { AuthServiceMock } from '../shared/mocks/auth.service.mock';
|
||||
import { MetadataServiceMock } from '../shared/mocks/metadata-service.mock';
|
||||
import { RouterMock } from '../shared/mocks/router.mock';
|
||||
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsBoardComponent } from '../shared/notifications/notifications-board/notifications-board.component';
|
||||
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
||||
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
|
||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
||||
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
||||
import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banner/system-wide-alert-banner.component';
|
||||
import { RootComponent } from './root.component';
|
||||
|
||||
@@ -57,29 +33,14 @@ describe('RootComponent', () => {
|
||||
imports: [
|
||||
CommonModule,
|
||||
NoopAnimationsModule,
|
||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
TranslateModule.forRoot(),
|
||||
RootComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
{ provide: MenuService, useValue: new MenuServiceStub() },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: LocaleService, useValue: {} },
|
||||
provideMockStore({ core: { auth: { loading: false } } } as any),
|
||||
RootComponent,
|
||||
RouteService,
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
|
||||
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
|
||||
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
|
||||
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
|
||||
'd-none': this.metadataService.isVirtual(value) && (model.hasSelectableMetadata || context?.index > 0)}">
|
||||
<div [ngClass]="getClass('grid', 'control')">
|
||||
<div>
|
||||
<ng-container #componentViewContainer></ng-container>
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
|
||||
<ng-container *ngIf="value?.isVirtual">
|
||||
<ng-container *ngIf="this.metadataService.isVirtual(value)">
|
||||
<ds-existing-metadata-list-element
|
||||
*ngIf="model.hasSelectableMetadata"
|
||||
[reoRel]="relationshipValue$ | async"
|
||||
|
@@ -62,7 +62,6 @@ import {
|
||||
APP_DATA_SERVICES_MAP,
|
||||
} from '../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
|
||||
@@ -71,7 +70,6 @@ import { VocabularyOptions } from '../../../../core/submission/vocabularies/mode
|
||||
import { SubmissionService } from '../../../../submission/submission.service';
|
||||
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { FormService } from '../../form.service';
|
||||
import { FormBuilderService } from '../form-builder.service';
|
||||
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
|
||||
import { dsDynamicFormControlMapFn } from './ds-dynamic-form-control-map-fn';
|
||||
@@ -228,11 +226,9 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
{ provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() },
|
||||
{ provide: RelationshipDataService, useValue: {} },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RelationshipDataService, useValue: {} },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: FormService, useValue: {} },
|
||||
{ provide: FormBuilderService, useValue: {} },
|
||||
{ provide: SubmissionService, useValue: {} },
|
||||
{
|
||||
@@ -241,7 +237,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)),
|
||||
},
|
||||
},
|
||||
{ provide: NgZone, useValue: new NgZone({}) },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
|
||||
|
@@ -14,7 +14,6 @@ import {
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
@@ -77,10 +76,10 @@ import {
|
||||
AppConfig,
|
||||
} from '../../../../../config/app-config.interface';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { MetadataService } from '../../../../core/metadata/metadata.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
@@ -112,7 +111,6 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele
|
||||
import { SearchResult } from '../../../search/models/search-result.model';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
import { itemLinksToFollow } from '../../../utils/relation-query.utils';
|
||||
import { FormService } from '../../form.service';
|
||||
import { FormBuilderService } from '../form-builder.service';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { RelationshipOptions } from '../models/relationship-options.model';
|
||||
@@ -202,17 +200,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
protected typeBindRelationService: DsDynamicTypeBindRelationService,
|
||||
protected translateService: TranslateService,
|
||||
protected relationService: DynamicFormRelationService,
|
||||
private modalService: NgbModal,
|
||||
private relationshipService: RelationshipDataService,
|
||||
private selectableListService: SelectableListService,
|
||||
private itemService: ItemDataService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>,
|
||||
private submissionObjectService: SubmissionObjectDataService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private formService: FormService,
|
||||
public formBuilderService: FormBuilderService,
|
||||
private submissionService: SubmissionService,
|
||||
protected modalService: NgbModal,
|
||||
protected relationshipService: RelationshipDataService,
|
||||
protected selectableListService: SelectableListService,
|
||||
protected store: Store<AppState>,
|
||||
protected submissionObjectService: SubmissionObjectDataService,
|
||||
protected ref: ChangeDetectorRef,
|
||||
protected formBuilderService: FormBuilderService,
|
||||
protected submissionService: SubmissionService,
|
||||
protected metadataService: MetadataService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
|
||||
) {
|
||||
@@ -277,8 +273,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
this.value = Object.assign(new FormFieldMetadataValueObject(), this.model.value);
|
||||
}
|
||||
|
||||
if (hasValue(this.value) && this.value.isVirtual) {
|
||||
const relationship$ = this.relationshipService.findById(this.value.virtualValue,
|
||||
if (hasValue(this.value) && this.metadataService.isVirtual(this.value)) {
|
||||
const relationship$ = this.relationshipService.findById(this.metadataService.virtualValue(this.value),
|
||||
true,
|
||||
true,
|
||||
... itemLinksToFollow(this.fetchThumbnail)).pipe(
|
||||
|
@@ -1,5 +1,5 @@
|
||||
|
||||
export class MetadataServiceMock {
|
||||
export class HeadTagServiceMock {
|
||||
|
||||
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
|
||||
public listenForRouteChange(): void {
|
@@ -34,7 +34,7 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
||||
import { coreSelector } from '../../app/core/core.selectors';
|
||||
import { RootDataService } from '../../app/core/data/root-data.service';
|
||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||
import { HeadTagService } from '../../app/core/metadata/head-tag.service';
|
||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||
import { InitService } from '../../app/init.service';
|
||||
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||
@@ -73,7 +73,7 @@ export class BrowserInitService extends InitService {
|
||||
protected localeService: LocaleService,
|
||||
protected angulartics2DSpace: Angulartics2DSpace,
|
||||
protected googleAnalyticsService: GoogleAnalyticsService,
|
||||
protected metadata: MetadataService,
|
||||
protected headTagService: HeadTagService,
|
||||
protected breadcrumbsService: BreadcrumbsService,
|
||||
protected klaroService: KlaroService,
|
||||
protected authService: AuthService,
|
||||
@@ -89,7 +89,7 @@ export class BrowserInitService extends InitService {
|
||||
translate,
|
||||
localeService,
|
||||
angulartics2DSpace,
|
||||
metadata,
|
||||
headTagService,
|
||||
breadcrumbsService,
|
||||
themeService,
|
||||
menuService,
|
||||
|
@@ -18,7 +18,7 @@ import { take } from 'rxjs/operators';
|
||||
import { AppState } from '../../app/app.reducer';
|
||||
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||
import { HeadTagService } from '../../app/core/metadata/head-tag.service';
|
||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||
import { InitService } from '../../app/init.service';
|
||||
import { MenuService } from '../../app/shared/menu/menu.service';
|
||||
@@ -44,7 +44,7 @@ export class ServerInitService extends InitService {
|
||||
protected translate: TranslateService,
|
||||
protected localeService: LocaleService,
|
||||
protected angulartics2DSpace: Angulartics2DSpace,
|
||||
protected metadata: MetadataService,
|
||||
protected headTagService: HeadTagService,
|
||||
protected breadcrumbsService: BreadcrumbsService,
|
||||
protected themeService: ThemeService,
|
||||
protected menuService: MenuService,
|
||||
@@ -56,7 +56,7 @@ export class ServerInitService extends InitService {
|
||||
translate,
|
||||
localeService,
|
||||
angulartics2DSpace,
|
||||
metadata,
|
||||
headTagService,
|
||||
breadcrumbsService,
|
||||
themeService,
|
||||
menuService,
|
||||
|
Reference in New Issue
Block a user