mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33: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 { authReducer } from './core/auth/auth.reducer';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { LocaleService } from './core/locale/locale.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 { RouteService } from './core/services/route.service';
|
||||||
import {
|
import {
|
||||||
NativeWindowRef,
|
NativeWindowRef,
|
||||||
@@ -42,7 +42,7 @@ import { MenuService } from './shared/menu/menu.service';
|
|||||||
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
||||||
import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock';
|
import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock';
|
||||||
import { AuthServiceMock } from './shared/mocks/auth.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 { RouterMock } from './shared/mocks/router.mock';
|
||||||
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||||
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
||||||
@@ -87,7 +87,7 @@ describe('App component', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: MetadataService, useValue: new MetadataServiceMock() },
|
{ provide: HeadTagService, useValue: new HeadTagServiceMock() },
|
||||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ 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 { 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 { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +15,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
|
|||||||
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.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 { PageInfo } from '../shared/page-info.model';
|
||||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
import { FindListOptions } from './find-list-options.model';
|
import { FindListOptions } from './find-list-options.model';
|
||||||
|
import { ItemDataService } from './item-data.service';
|
||||||
import { buildPaginatedList } from './paginated-list.model';
|
import { buildPaginatedList } from './paginated-list.model';
|
||||||
import { RelationshipDataService } from './relationship-data.service';
|
import { RelationshipDataService } from './relationship-data.service';
|
||||||
import { DeleteRequest } from './request.models';
|
import { DeleteRequest } from './request.models';
|
||||||
@@ -123,18 +130,6 @@ describe('RelationshipDataService', () => {
|
|||||||
findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]),
|
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) => {
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
return observableOf({
|
return observableOf({
|
||||||
response: { isSuccessful: successful, payload: relationships } as any,
|
response: { isSuccessful: successful, payload: relationships } as any,
|
||||||
@@ -143,11 +138,25 @@ describe('RelationshipDataService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
requestService = getMockRequestService(getRequestEntry$(true));
|
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', () => {
|
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);
|
testSearchDataImplementation(initService);
|
||||||
});
|
});
|
||||||
|
@@ -54,6 +54,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { MetadataService } from '../metadata/metadata.service';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
@@ -128,6 +129,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected metadataService: MetadataService,
|
||||||
protected itemService: ItemDataService,
|
protected itemService: ItemDataService,
|
||||||
protected appStore: Store<AppState>,
|
protected appStore: Store<AppState>,
|
||||||
@Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>) => Observable<RemoteData<PaginatedList<Item>>>,
|
@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)
|
* @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> {
|
resolveMetadataRepresentation(metadatum: MetadataValue, parentItem: DSpaceObject, itemType: string): Observable<MetadataRepresentation> {
|
||||||
if (metadatum.isVirtual) {
|
if (this.metadataService.isVirtual(metadatum)) {
|
||||||
return this.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe(
|
return this.findById(this.metadataService.virtualValue(metadatum), true, false, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
switchMap((relRD: RemoteData<Relationship>) =>
|
switchMap((relRD: RemoteData<Relationship>) =>
|
||||||
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
|
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 {
|
import { TestBed } from '@angular/core/testing';
|
||||||
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 {
|
|
||||||
AddMetaTagAction,
|
|
||||||
ClearMetaTagAction,
|
|
||||||
} from './meta-tag.actions';
|
|
||||||
import { MetadataService } from './metadata.service';
|
import { MetadataService } from './metadata.service';
|
||||||
|
|
||||||
describe('MetadataService', () => {
|
describe('MetadataService', () => {
|
||||||
let metadataService: MetadataService;
|
let service: 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'] } } };
|
|
||||||
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootService = jasmine.createSpyObj({
|
TestBed.configureTestingModule({});
|
||||||
findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' }),
|
service = TestBed.inject(MetadataService);
|
||||||
});
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('items page should set meta tags', fakeAsync(() => {
|
it('should be created', () => {
|
||||||
(metadataService as any).processRouteChange({
|
expect(service).toBeTruthy();
|
||||||
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',
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
import { Injectable } from '@angular/core';
|
||||||
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 { hasValue } from '../../shared/empty.util';
|
||||||
import {
|
import {
|
||||||
APP_CONFIG,
|
MetadataValue,
|
||||||
AppConfig,
|
VIRTUAL_METADATA_PREFIX,
|
||||||
} from '../../../config/app-config.interface';
|
} from '../shared/metadata.models';
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base selector function to select the metaTag section in the store
|
* Service for working with DSpace object metadata.
|
||||||
*/
|
*/
|
||||||
const metaTagSelector = createSelector(
|
@Injectable({
|
||||||
coreSelector,
|
providedIn: 'root',
|
||||||
(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 MetadataService {
|
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),
|
* Returns true if this Metadata authority key starts with 'virtual::'
|
||||||
* the first Bitstream to match one of the following MIME types is selected.
|
|
||||||
* See {@linkcode getFirstAllowedFormatBitstreamLink}
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
private readonly CITATION_PDF_URL_MIMETYPES = [
|
public isVirtual(metadataValue: MetadataValue | undefined): boolean {
|
||||||
'application/pdf', // .pdf
|
return hasValue(metadataValue?.authority) && metadataValue.authority.startsWith(VIRTUAL_METADATA_PREFIX);
|
||||||
'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();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
public virtualValue(metadataValue: MetadataValue | undefined): string {
|
||||||
const value = this.dsoNameService.getName(this.currentObject.getValue());
|
if (this.isVirtual) {
|
||||||
this.addMetaTag('title', value);
|
return metadataValue.authority.substring(metadataValue.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length);
|
||||||
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);
|
|
||||||
} else {
|
} 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';
|
} from 'cerialize';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
|
|
||||||
export const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
export const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
||||||
|
|
||||||
/** A single metadata value and its properties. */
|
/** A single metadata value and its properties. */
|
||||||
@@ -58,24 +56,6 @@ export class MetadataValue implements MetadataValueInterface {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
confidence: number;
|
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. */
|
/** 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)"
|
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 }">
|
[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">
|
<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 { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { RelationshipDataService } from '../../../core/data/relationship-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 { Collection } from '../../../core/shared/collection.model';
|
||||||
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
import { ConfidenceType } from '../../../core/shared/confidence-type';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
@@ -212,14 +213,17 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
private isScrollableVocabulary$: Observable<boolean>;
|
private isScrollableVocabulary$: Observable<boolean>;
|
||||||
private isSuggesterVocabulary$: Observable<boolean>;
|
private isSuggesterVocabulary$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(protected relationshipService: RelationshipDataService,
|
constructor(
|
||||||
protected dsoNameService: DSONameService,
|
protected relationshipService: RelationshipDataService,
|
||||||
protected vocabularyService: VocabularyService,
|
protected dsoNameService: DSONameService,
|
||||||
protected itemService: ItemDataService,
|
protected vocabularyService: VocabularyService,
|
||||||
protected cdr: ChangeDetectorRef,
|
protected itemService: ItemDataService,
|
||||||
protected registryService: RegistryService,
|
protected cdr: ChangeDetectorRef,
|
||||||
protected notificationsService: NotificationsService,
|
protected registryService: RegistryService,
|
||||||
protected translate: TranslateService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected metadataService: MetadataService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -231,7 +235,7 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges {
|
|||||||
* Initialise potential properties of a virtual metadata value
|
* Initialise potential properties of a virtual metadata value
|
||||||
*/
|
*/
|
||||||
initVirtualProperties(): void {
|
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')
|
this.relationshipService.resolveMetadataRepresentation(this.mdValue.newValue, this.dso, 'Item')
|
||||||
.pipe(
|
.pipe(
|
||||||
map((mdRepresentation: MetadataRepresentation) =>
|
map((mdRepresentation: MetadataRepresentation) =>
|
||||||
|
@@ -31,7 +31,6 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
|||||||
import { authReducer } from './core/auth/auth.reducer';
|
import { authReducer } from './core/auth/auth.reducer';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
import { MetadataService } from './core/metadata/metadata.service';
|
|
||||||
import { RouteService } from './core/services/route.service';
|
import { RouteService } from './core/services/route.service';
|
||||||
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||||
import { InitService } from './init.service';
|
import { InitService } from './init.service';
|
||||||
@@ -49,6 +48,9 @@ import createSpyObj = jasmine.createSpyObj;
|
|||||||
import SpyObj = jasmine.SpyObj;
|
import SpyObj = jasmine.SpyObj;
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
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>;
|
let spy: SpyObj<any>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -138,7 +140,7 @@ describe('InitService', () => {
|
|||||||
let correlationIdServiceSpy;
|
let correlationIdServiceSpy;
|
||||||
let dspaceTransferStateSpy;
|
let dspaceTransferStateSpy;
|
||||||
let transferStateSpy;
|
let transferStateSpy;
|
||||||
let metadataServiceSpy;
|
let headTagService: HeadTagServiceMock;
|
||||||
let breadcrumbsServiceSpy;
|
let breadcrumbsServiceSpy;
|
||||||
let menuServiceSpy;
|
let menuServiceSpy;
|
||||||
|
|
||||||
@@ -164,9 +166,7 @@ describe('InitService', () => {
|
|||||||
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
||||||
'listenForRouteChanges',
|
'listenForRouteChanges',
|
||||||
]);
|
]);
|
||||||
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
|
headTagService = new HeadTagServiceMock();
|
||||||
'listenForRouteChange',
|
|
||||||
]);
|
|
||||||
menuServiceSpy = jasmine.createSpyObj('menuServiceSpy', [
|
menuServiceSpy = jasmine.createSpyObj('menuServiceSpy', [
|
||||||
'listenForRouteChanges',
|
'listenForRouteChanges',
|
||||||
]);
|
]);
|
||||||
@@ -190,7 +190,7 @@ describe('InitService', () => {
|
|||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||||
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||||
{ provide: MetadataService, useValue: metadataServiceSpy },
|
{ provide: HeadTagService, useValue: headTagService },
|
||||||
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
@@ -206,9 +206,9 @@ describe('InitService', () => {
|
|||||||
|
|
||||||
describe('initRouteListeners', () => {
|
describe('initRouteListeners', () => {
|
||||||
it('should call listenForRouteChanges', inject([InitService], (service) => {
|
it('should call listenForRouteChanges', inject([InitService], (service) => {
|
||||||
// @ts-ignore
|
spyOn(headTagService, 'listenForRouteChange');
|
||||||
service.initRouteListeners();
|
service.initRouteListeners();
|
||||||
expect(metadataServiceSpy.listenForRouteChange).toHaveBeenCalledTimes(1);
|
expect(headTagService.listenForRouteChange).toHaveBeenCalledTimes(1);
|
||||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
expect(breadcrumbsServiceSpy.listenForRouteChanges).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 { isAuthenticationBlocking } from './core/auth/selectors';
|
||||||
import { LAZY_DATA_SERVICES } from './core/data-services-map';
|
import { LAZY_DATA_SERVICES } from './core/data-services-map';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
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 { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||||
import { dsDynamicFormControlMapFn } from './shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn';
|
import { dsDynamicFormControlMapFn } from './shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
@@ -70,7 +70,7 @@ export abstract class InitService {
|
|||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected localeService: LocaleService,
|
protected localeService: LocaleService,
|
||||||
protected angulartics2DSpace: Angulartics2DSpace,
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
protected metadata: MetadataService,
|
protected headTagService: HeadTagService,
|
||||||
protected breadcrumbsService: BreadcrumbsService,
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
protected themeService: ThemeService,
|
protected themeService: ThemeService,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
@@ -207,13 +207,13 @@ export abstract class InitService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start route-listening subscriptions
|
* Start route-listening subscriptions
|
||||||
* - {@link MetadataService.listenForRouteChange}
|
* - {@link HeadTagService.listenForRouteChange}
|
||||||
* - {@link BreadcrumbsService.listenForRouteChanges}
|
* - {@link BreadcrumbsService.listenForRouteChanges}
|
||||||
* - {@link ThemeService.listenForRouteChanges}
|
* - {@link ThemeService.listenForRouteChanges}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected initRouteListeners(): void {
|
protected initRouteListeners(): void {
|
||||||
this.metadata.listenForRouteChange();
|
this.headTagService.listenForRouteChange();
|
||||||
this.breadcrumbsService.listenForRouteChanges();
|
this.breadcrumbsService.listenForRouteChanges();
|
||||||
this.themeService.listenForRouteChanges();
|
this.themeService.listenForRouteChanges();
|
||||||
this.menuService.listenForRouteChanges();
|
this.menuService.listenForRouteChanges();
|
||||||
|
@@ -28,12 +28,13 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
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 { LinkHeadService } from '../../core/services/link-head.service';
|
||||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.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 { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
import {
|
import {
|
||||||
@@ -74,13 +75,6 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), {
|
|||||||
isWithdrawn: true,
|
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', () => {
|
describe('FullItemPageComponent', () => {
|
||||||
let comp: FullItemPageComponent;
|
let comp: FullItemPageComponent;
|
||||||
let fixture: ComponentFixture<FullItemPageComponent>;
|
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||||
@@ -93,6 +87,7 @@ describe('FullItemPageComponent', () => {
|
|||||||
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
|
||||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||||
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
|
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
|
||||||
|
let headTagService: HeadTagServiceMock;
|
||||||
|
|
||||||
const mocklink = {
|
const mocklink = {
|
||||||
href: 'http://test.org',
|
href: 'http://test.org',
|
||||||
@@ -143,6 +138,8 @@ describe('FullItemPageComponent', () => {
|
|||||||
getInboxRelationLink: observableOf('http://test.org'),
|
getInboxRelationLink: observableOf('http://test.org'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
headTagService = new HeadTagServiceMock();
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
@@ -153,7 +150,7 @@ describe('FullItemPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
{ provide: MetadataService, useValue: metadataServiceStub },
|
{ provide: HeadTagService, useValue: headTagService },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
{ provide: ServerResponseService, useValue: serverResponseService },
|
{ provide: ServerResponseService, useValue: serverResponseService },
|
||||||
|
@@ -26,7 +26,6 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
|||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
||||||
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
import { SignpostingLink } from '../../core/data/signposting-links.model';
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
|
||||||
import {
|
import {
|
||||||
LinkDefinition,
|
LinkDefinition,
|
||||||
LinkHeadService,
|
LinkHeadService,
|
||||||
@@ -92,12 +91,6 @@ describe('ItemPageComponent', () => {
|
|||||||
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
let linkHeadService: jasmine.SpyObj<LinkHeadService>;
|
||||||
let notifyInfoService: jasmine.SpyObj<NotifyInfoService>;
|
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(), {
|
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
||||||
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }),
|
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }),
|
||||||
});
|
});
|
||||||
@@ -141,7 +134,6 @@ describe('ItemPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: mockRoute },
|
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
{ provide: MetadataService, useValue: mockMetadataService },
|
|
||||||
{ provide: Router, useValue: {} },
|
{ provide: Router, useValue: {} },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
|
@@ -17,6 +17,7 @@ import { map } from 'rxjs/operators';
|
|||||||
import { BrowseService } from '../../../core/browse/browse.service';
|
import { BrowseService } from '../../../core/browse/browse.service';
|
||||||
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
|
import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service';
|
||||||
import { RelationshipDataService } from '../../../core/data/relationship-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 { Item } from '../../../core/shared/item.model';
|
||||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||||
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
@@ -76,6 +77,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
|||||||
constructor(
|
constructor(
|
||||||
public relationshipService: RelationshipDataService,
|
public relationshipService: RelationshipDataService,
|
||||||
protected browseDefinitionDataService: BrowseDefinitionDataService,
|
protected browseDefinitionDataService: BrowseDefinitionDataService,
|
||||||
|
protected metadataService: MetadataService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -101,7 +103,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
|||||||
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
|
.slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy)
|
||||||
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
||||||
.map((metadatum: MetadataValue) => {
|
.map((metadatum: MetadataValue) => {
|
||||||
if (metadatum.isVirtual) {
|
if (this.metadataService.isVirtual(metadatum)) {
|
||||||
return this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType);
|
return this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType);
|
||||||
} else {
|
} else {
|
||||||
// Check for a configured browse link and return a standard metadata representation
|
// Check for a configured browse link and return a standard metadata representation
|
||||||
|
@@ -5,46 +5,22 @@ import {
|
|||||||
TestBed,
|
TestBed,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import {
|
import { Router } from '@angular/router';
|
||||||
ActivatedRoute,
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { StoreModule } from '@ngrx/store';
|
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
|
||||||
import {
|
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component';
|
import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component';
|
||||||
import { storeModuleConfig } from '../app.reducer';
|
|
||||||
import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component';
|
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 { ThemedFooterComponent } from '../footer/themed-footer.component';
|
||||||
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
|
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
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 { 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 { NotificationsBoardComponent } from '../shared/notifications/notifications-board/notifications-board.component';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
|
||||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||||
import { MenuServiceStub } from '../shared/testing/menu-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 { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banner/system-wide-alert-banner.component';
|
||||||
import { RootComponent } from './root.component';
|
import { RootComponent } from './root.component';
|
||||||
|
|
||||||
@@ -57,29 +33,14 @@ describe('RootComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
TranslateModule.forRoot(),
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
RootComponent,
|
RootComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
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: Router, useValue: new RouterMock() },
|
||||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
|
||||||
{ provide: MenuService, useValue: new MenuServiceStub() },
|
{ provide: MenuService, useValue: new MenuServiceStub() },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
{ provide: LocaleService, useValue: {} },
|
|
||||||
provideMockStore({ core: { auth: { loading: false } } } as any),
|
|
||||||
RootComponent,
|
|
||||||
RouteService,
|
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
|
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: { $implicit: model };"></ng-container>
|
||||||
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
|
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
|
||||||
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
|
<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 [ngClass]="getClass('grid', 'control')">
|
||||||
<div>
|
<div>
|
||||||
<ng-container #componentViewContainer></ng-container>
|
<ng-container #componentViewContainer></ng-container>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
|
<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
|
<ds-existing-metadata-list-element
|
||||||
*ngIf="model.hasSelectableMetadata"
|
*ngIf="model.hasSelectableMetadata"
|
||||||
[reoRel]="relationshipValue$ | async"
|
[reoRel]="relationshipValue$ | async"
|
||||||
|
@@ -62,7 +62,6 @@ import {
|
|||||||
APP_DATA_SERVICES_MAP,
|
APP_DATA_SERVICES_MAP,
|
||||||
} from '../../../../../config/app-config.interface';
|
} from '../../../../../config/app-config.interface';
|
||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
|
||||||
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.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 { SubmissionService } from '../../../../submission/submission.service';
|
||||||
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
import { FormService } from '../../form.service';
|
|
||||||
import { FormBuilderService } from '../form-builder.service';
|
import { FormBuilderService } from '../form-builder.service';
|
||||||
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
|
import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-control-container.component';
|
||||||
import { dsDynamicFormControlMapFn } from './ds-dynamic-form-control-map-fn';
|
import { dsDynamicFormControlMapFn } from './ds-dynamic-form-control-map-fn';
|
||||||
@@ -228,11 +226,9 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
|||||||
{ provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() },
|
{ provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() },
|
||||||
{ provide: RelationshipDataService, useValue: {} },
|
{ provide: RelationshipDataService, useValue: {} },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RelationshipDataService, useValue: {} },
|
{ provide: RelationshipDataService, useValue: {} },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
{ provide: FormService, useValue: {} },
|
|
||||||
{ provide: FormBuilderService, useValue: {} },
|
{ provide: FormBuilderService, useValue: {} },
|
||||||
{ provide: SubmissionService, useValue: {} },
|
{ provide: SubmissionService, useValue: {} },
|
||||||
{
|
{
|
||||||
@@ -241,7 +237,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
|||||||
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)),
|
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ provide: NgZone, useValue: new NgZone({}) },
|
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
|
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
Inject,
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
NgZone,
|
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -77,10 +76,10 @@ import {
|
|||||||
AppConfig,
|
AppConfig,
|
||||||
} from '../../../../../config/app-config.interface';
|
} from '../../../../../config/app-config.interface';
|
||||||
import { AppState } from '../../../../app.reducer';
|
import { AppState } from '../../../../app.reducer';
|
||||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { MetadataService } from '../../../../core/metadata/metadata.service';
|
||||||
import { Collection } from '../../../../core/shared/collection.model';
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { Item } from '../../../../core/shared/item.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 { SearchResult } from '../../../search/models/search-result.model';
|
||||||
import { followLink } from '../../../utils/follow-link-config.model';
|
import { followLink } from '../../../utils/follow-link-config.model';
|
||||||
import { itemLinksToFollow } from '../../../utils/relation-query.utils';
|
import { itemLinksToFollow } from '../../../utils/relation-query.utils';
|
||||||
import { FormService } from '../../form.service';
|
|
||||||
import { FormBuilderService } from '../form-builder.service';
|
import { FormBuilderService } from '../form-builder.service';
|
||||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
import { RelationshipOptions } from '../models/relationship-options.model';
|
import { RelationshipOptions } from '../models/relationship-options.model';
|
||||||
@@ -202,17 +200,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
|||||||
protected typeBindRelationService: DsDynamicTypeBindRelationService,
|
protected typeBindRelationService: DsDynamicTypeBindRelationService,
|
||||||
protected translateService: TranslateService,
|
protected translateService: TranslateService,
|
||||||
protected relationService: DynamicFormRelationService,
|
protected relationService: DynamicFormRelationService,
|
||||||
private modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
private relationshipService: RelationshipDataService,
|
protected relationshipService: RelationshipDataService,
|
||||||
private selectableListService: SelectableListService,
|
protected selectableListService: SelectableListService,
|
||||||
private itemService: ItemDataService,
|
protected store: Store<AppState>,
|
||||||
private zone: NgZone,
|
protected submissionObjectService: SubmissionObjectDataService,
|
||||||
private store: Store<AppState>,
|
protected ref: ChangeDetectorRef,
|
||||||
private submissionObjectService: SubmissionObjectDataService,
|
protected formBuilderService: FormBuilderService,
|
||||||
private ref: ChangeDetectorRef,
|
protected submissionService: SubmissionService,
|
||||||
private formService: FormService,
|
protected metadataService: MetadataService,
|
||||||
public formBuilderService: FormBuilderService,
|
|
||||||
private submissionService: SubmissionService,
|
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
@Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn,
|
@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);
|
this.value = Object.assign(new FormFieldMetadataValueObject(), this.model.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasValue(this.value) && this.value.isVirtual) {
|
if (hasValue(this.value) && this.metadataService.isVirtual(this.value)) {
|
||||||
const relationship$ = this.relationshipService.findById(this.value.virtualValue,
|
const relationship$ = this.relationshipService.findById(this.metadataService.virtualValue(this.value),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
... itemLinksToFollow(this.fetchThumbnail)).pipe(
|
... 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
|
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
|
||||||
public listenForRouteChange(): void {
|
public listenForRouteChange(): void {
|
@@ -34,7 +34,7 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
|||||||
import { coreSelector } from '../../app/core/core.selectors';
|
import { coreSelector } from '../../app/core/core.selectors';
|
||||||
import { RootDataService } from '../../app/core/data/root-data.service';
|
import { RootDataService } from '../../app/core/data/root-data.service';
|
||||||
import { LocaleService } from '../../app/core/locale/locale.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 { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||||
import { InitService } from '../../app/init.service';
|
import { InitService } from '../../app/init.service';
|
||||||
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
@@ -73,7 +73,7 @@ export class BrowserInitService extends InitService {
|
|||||||
protected localeService: LocaleService,
|
protected localeService: LocaleService,
|
||||||
protected angulartics2DSpace: Angulartics2DSpace,
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
protected googleAnalyticsService: GoogleAnalyticsService,
|
protected googleAnalyticsService: GoogleAnalyticsService,
|
||||||
protected metadata: MetadataService,
|
protected headTagService: HeadTagService,
|
||||||
protected breadcrumbsService: BreadcrumbsService,
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
protected klaroService: KlaroService,
|
protected klaroService: KlaroService,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
@@ -89,7 +89,7 @@ export class BrowserInitService extends InitService {
|
|||||||
translate,
|
translate,
|
||||||
localeService,
|
localeService,
|
||||||
angulartics2DSpace,
|
angulartics2DSpace,
|
||||||
metadata,
|
headTagService,
|
||||||
breadcrumbsService,
|
breadcrumbsService,
|
||||||
themeService,
|
themeService,
|
||||||
menuService,
|
menuService,
|
||||||
|
@@ -18,7 +18,7 @@ import { take } from 'rxjs/operators';
|
|||||||
import { AppState } from '../../app/app.reducer';
|
import { AppState } from '../../app/app.reducer';
|
||||||
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||||
import { LocaleService } from '../../app/core/locale/locale.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 { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||||
import { InitService } from '../../app/init.service';
|
import { InitService } from '../../app/init.service';
|
||||||
import { MenuService } from '../../app/shared/menu/menu.service';
|
import { MenuService } from '../../app/shared/menu/menu.service';
|
||||||
@@ -44,7 +44,7 @@ export class ServerInitService extends InitService {
|
|||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected localeService: LocaleService,
|
protected localeService: LocaleService,
|
||||||
protected angulartics2DSpace: Angulartics2DSpace,
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
protected metadata: MetadataService,
|
protected headTagService: HeadTagService,
|
||||||
protected breadcrumbsService: BreadcrumbsService,
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
protected themeService: ThemeService,
|
protected themeService: ThemeService,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
@@ -56,7 +56,7 @@ export class ServerInitService extends InitService {
|
|||||||
translate,
|
translate,
|
||||||
localeService,
|
localeService,
|
||||||
angulartics2DSpace,
|
angulartics2DSpace,
|
||||||
metadata,
|
headTagService,
|
||||||
breadcrumbsService,
|
breadcrumbsService,
|
||||||
themeService,
|
themeService,
|
||||||
menuService,
|
menuService,
|
||||||
|
Reference in New Issue
Block a user