From f562828459f8ebdb17ffa843130856cad6d93dc5 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Wed, 20 Nov 2024 11:32:16 +0100 Subject: [PATCH] [DSC-2036] port fix for metadata service --- .../core/metadata/metadata.service.spec.ts | 173 ++++++++++-------- src/app/core/metadata/metadata.service.ts | 86 ++++++++- src/app/shared/mocks/item.mock.ts | 12 ++ src/index.html | 20 +- 4 files changed, 198 insertions(+), 93 deletions(-) diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index e8d45907c6a..c121237f324 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { fakeAsync, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { Meta, Title } from '@angular/platform-browser'; import { NavigationEnd, Router } from '@angular/router'; @@ -31,8 +31,9 @@ import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; import { SchemaJsonLDService } from './schema-json-ld/schema-json-ld.service'; +import { DOCUMENT } from '@angular/common'; -xdescribe('MetadataService', () => { +describe('MetadataService', () => { let metadataService: MetadataService; let meta: Meta; @@ -42,7 +43,6 @@ xdescribe('MetadataService', () => { let dsoNameService: DSONameService; let bundleDataService; - let bitstreamDataService; let rootService: RootDataService; let translateService: TranslateService; let hardRedirectService: HardRedirectService; @@ -53,22 +53,35 @@ xdescribe('MetadataService', () => { let store; let appConfig: AppConfig; + let _document: any; + let platformId: string; + const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; + const createSuccessfulRemoteDataObjectAndAssignThumbnail = (dso: Item) => { + const bitstream = Object.assign(new Bitstream(), { + uuid: 'thumbnail-uuid', + _links: { + self: { href: 'thumbnail-url' }, + }, + }); + const dsoWithThumbnail = Object.assign(dso, { thumbnail: createSuccessfulRemoteDataObject$(MockBitstream3) }); + return createSuccessfulRemoteDataObject(dsoWithThumbnail); + }; + + beforeEach(() => { rootService = jasmine.createSpyObj({ findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version', crisVersion: 'mock-cris-version' }), }); - bitstreamDataService = jasmine.createSpyObj({ - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3])), - }); bundleDataService = jasmine.createSpyObj({ findByItemAndName: mockBundleRD$([MockBitstream3]) }); translateService = getMockTranslateService(); meta = jasmine.createSpyObj('meta', { + updateTag: {}, addTag: {}, removeTag: {} }); @@ -108,6 +121,10 @@ xdescribe('MetadataService', () => { } } as any; + platformId = 'browser'; + _document = TestBed.inject(DOCUMENT); + + metadataService = new MetadataService( router, translateService, @@ -115,16 +132,14 @@ xdescribe('MetadataService', () => { title, dsoNameService, bundleDataService, - bitstreamDataService, - undefined, rootService, store, hardRedirectService, appConfig, authorizationService, schemaJsonLDService, - 'browser', - null + platformId, + _document, ); }); @@ -132,26 +147,25 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), } } }); - tick(); expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document'); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_title', - content: 'Test PowerPoint Document' + content: 'Test PowerPoint Document', }); - expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' }); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' }); + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_publication_date', - content: '1650-06-26' + 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({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' }); + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' }); + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_keywords', - content: 'keyword1; keyword2; keyword3' + content: 'keyword1; keyword2; keyword3', }); })); @@ -159,17 +173,18 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockPublisher(mockType(ItemMock, 'Thesis'))), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_dissertation_name', content: 'Test PowerPoint Document' }); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_pdf_url', + property: 'citation_pdf_url', content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download' }); })); @@ -178,35 +193,53 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockPublisher(mockType(ItemMock, 'Technical Report'))), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).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')); + (translateService.get as jasmine.Spy).and.callFake((key: string) => { + if (key.includes('route.title.key')) { + return of('Translated Route Title'); + } else if (key.includes('repository.title.prefix')) { + return of('DSpace :: '); + } else { + return of(key); + } + }); (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), title: 'route.title.key', } } }); tick(); - expect(title.setTitle).toHaveBeenCalledTimes(2); + expect(title.setTitle).toHaveBeenCalledTimes(3); 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']); + expect((title.setTitle as jasmine.Spy).calls.argsFor(2)).toEqual(['Test PowerPoint Document']); })); 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!')); + (translateService.get as jasmine.Spy).and.callFake((key: string) => { + if (key.includes('route.title.key')) { + return of('Dummy Title'); + } else if (key.includes('repository.title.prefix')) { + return of('DSpace :: '); + } else { + return of(key); + } + }); + (metadataService as any).processRouteChange({ data: { value: { @@ -217,11 +250,11 @@ xdescribe('MetadataService', () => { }); tick(); expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title'); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'title', content: 'DSpace :: Dummy Title' }); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'description', content: 'This is a dummy item component for testing!' }); @@ -254,12 +287,12 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockUri(ItemMock, 'https://ddg.gg')), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_abstract_html_url', content: 'https://ddg.gg' }); @@ -269,12 +302,12 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockUri(ItemMock)), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_abstract_html_url', content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357' }); @@ -286,48 +319,48 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockPublisher(mockType(ItemMock, 'Thesis'))), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).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' })); + expect(meta.updateTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); + expect(meta.updateTag).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'))), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(mockPublisher(mockType(ItemMock, 'Technical Report'))), } } }); tick(); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_technical_report_institution', content: 'Mock Publisher' }); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' })); + expect(meta.updateTag).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'))), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(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({ + expect(meta.updateTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); + expect(meta.updateTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_publisher', content: 'Mock Publisher' }); @@ -341,13 +374,14 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_pdf_url', + property: 'citation_pdf_url', content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download' }); })); @@ -360,12 +394,12 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), } } }); tick(); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' })); + expect(meta.updateTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' })); })); }); @@ -377,13 +411,14 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), } } }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_pdf_url', + property: 'citation_pdf_url', content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download' }); })); @@ -394,23 +429,21 @@ xdescribe('MetadataService', () => { 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), - } - } + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), + }, + }, }); tick(); - expect(meta.addTag).toHaveBeenCalledWith({ + expect(meta.updateTag).toHaveBeenCalledWith({ name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download' + property: 'citation_pdf_url', + content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download', }); })); @@ -425,22 +458,20 @@ xdescribe('MetadataService', () => { 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), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(ItemMock), } } }); tick(); - expect(meta.addTag).not.toHaveBeenCalledWith({ + expect(meta.updateTag).not.toHaveBeenCalledWith({ name: 'citation_pdf_url', + property: 'citation_pdf_url', content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download' }); })); @@ -453,22 +484,18 @@ xdescribe('MetadataService', () => { (metadataService as any).processRouteChange({ data: { value: { - dso: createSuccessfulRemoteDataObject(ItemMock), + dso: createSuccessfulRemoteDataObjectAndAssignThumbnail(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')]); + expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('og:title')]); }); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 513e54d3812..ccb1e5f8ba8 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -18,8 +18,6 @@ import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; -import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; import { RemoteData } from '../data/remote-data'; import { BitstreamFormat } from '../shared/bitstream-format.model'; @@ -46,6 +44,8 @@ import { ITEM } from '../shared/item.resource-type'; import { DOCUMENT, isPlatformBrowser, isPlatformServer } from '@angular/common'; import { Root } from '../data/root.model'; import { environment } from '../../../environments/environment'; +import { Bundle } from '../shared/bundle.model'; +import { followLink } from '../../shared/utils/follow-link-config.model'; /** * The base selector function to select the metaTag section in the store @@ -95,8 +95,6 @@ export class MetadataService { private title: Title, private dsoNameService: DSONameService, private bundleDataService: BundleDataService, - private bitstreamDataService: BitstreamDataService, - private bitstreamFormatDataService: BitstreamFormatDataService, private rootService: RootDataService, private store: Store, private hardRedirectService: HardRedirectService, @@ -449,7 +447,7 @@ export class MetadataService { * Add to the */ private setOpenGraphImageTag(): void { - this.setPrimaryBitstreamInBundleTag('og:image'); + this.setPrimaryImageInBundleTag('og:image'); } /** @@ -465,7 +463,7 @@ export class MetadataService { * Add to the */ private setTwitterImageTag(): void { - this.setPrimaryBitstreamInBundleTag('twitter:image'); + this.setPrimaryImageInBundleTag('twitter:image'); } /** @@ -496,6 +494,74 @@ export class MetadataService { } private setPrimaryBitstreamInBundleTag(tag: string): 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) => { + 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>) => { + 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 tag + this.addMetaTag( + tag, + new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(), + true, + ); + }); + } + } + + private setPrimaryImageInBundleTag(tag: string): void { if (this.currentObject.value instanceof Item) { const item = this.currentObject.value as Item; this.getBitstreamFromThumbnail(item).pipe( @@ -506,14 +572,14 @@ export class MetadataService { return null; } }), - take(1) + take(1), ).subscribe((link) => { if (hasValue(link)) { // Use the found link to set the tag this.addMetaTag( tag, new URLCombiner(this.getUrlOrigin(), link).toString(), - true + true, ); } else { this.addFallbackImageToTag(tag); @@ -654,9 +720,9 @@ export class MetadataService { return this.currentObject.value.allMetadataValues(keys); } - private addMetaTag(name: string, content: string, isProperty = false): void { + protected addMetaTag(name: string, content: string, isProperty = false): void { if (content) { - const tag = isProperty ? {property: name, content} as MetaDefinition + const tag = isProperty ? { name, property: name, content } as MetaDefinition : { name, content } as MetaDefinition; this.meta.updateTag(tag); this.storeTag(name); diff --git a/src/app/shared/mocks/item.mock.ts b/src/app/shared/mocks/item.mock.ts index 77685cca9ac..f74f97645ff 100644 --- a/src/app/shared/mocks/item.mock.ts +++ b/src/app/shared/mocks/item.mock.ts @@ -275,6 +275,18 @@ export const ItemMock: Item = Object.assign(new Item(), { language: 'en_US', value: 'text' } + ], + 'dc.relation.issn': [ + { + language: 'en_US', + value: '123456789', + }, + ], + 'dspace.entity.type': [ + { + language: 'en', + value: 'Publication', + }, ] }, owningCollection: observableOf({ diff --git a/src/index.html b/src/index.html index d1dd317278a..beb2d93e453 100644 --- a/src/index.html +++ b/src/index.html @@ -9,19 +9,19 @@ - - - - - - - - - + + + + + + + + + - +