diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 76529891d4b..607fecad499 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,10 +5,24 @@ import { Observable, of as observableOf, } from 'rxjs'; -import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators'; -import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util'; +import { + map, + switchMap, + filter, + distinctUntilKeyChanged, startWith, +} from 'rxjs/operators'; +import { + hasValue, + isEmpty, + isNotEmpty, + hasNoValue, + isUndefined, +} from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; +import { + FollowLinkConfig, + followLink, +} from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; import { RequestService } from '../../data/request.service'; @@ -29,10 +43,11 @@ import { getFirstCompletedRemoteData } from '../../shared/operators'; @Injectable() export class RemoteDataBuildService { - constructor(protected objectCache: ObjectCacheService, - protected linkService: LinkService, - protected requestService: RequestService) { - } + constructor( + protected objectCache: ObjectCacheService, + protected linkService: LinkService, + protected requestService: RequestService + ) {} /** * Creates an Observable with the payload for a RemoteData object @@ -46,21 +61,36 @@ export class RemoteDataBuildService { * should be automatically resolved * @private */ - private buildPayload(requestEntry$: Observable, href$?: Observable, ...linksToFollow: FollowLinkConfig[]): Observable { + private buildPayload( + requestEntry$: Observable, + href$?: Observable, + ...linksToFollow: FollowLinkConfig[] + ): Observable { if (hasNoValue(href$)) { href$ = observableOf(undefined); } return observableCombineLatest([href$, requestEntry$]).pipe( switchMap(([href, entry]: [string, RequestEntry]) => { - const hasExactMatchInObjectCache = this.hasExactMatchInObjectCache(href, entry); - if (hasValue(entry.response) && - (hasExactMatchInObjectCache || this.isCacheablePayload(entry) || this.isUnCacheablePayload(entry))) { + const hasExactMatchInObjectCache = this.hasExactMatchInObjectCache( + href, + entry + ); + if ( + hasValue(entry.response) && + (hasExactMatchInObjectCache || + this.isCacheablePayload(entry) || + this.isUnCacheablePayload(entry)) + ) { if (hasExactMatchInObjectCache) { return this.objectCache.getObjectByHref(href); } else if (this.isCacheablePayload(entry)) { - return this.objectCache.getObjectByHref(entry.response.payloadLink.href); + return this.objectCache.getObjectByHref( + entry.response.payloadLink.href + ); } else { - return [this.plainObjectToInstance(entry.response.unCacheableObject)]; + return [ + this.plainObjectToInstance(entry.response.unCacheableObject), + ]; } } else if (hasSucceeded(entry.state)) { return [null]; @@ -70,7 +100,9 @@ export class RemoteDataBuildService { }), switchMap((obj: T) => { if (hasValue(obj)) { - if (getResourceTypeValueFor((obj as any).type) === PAGINATED_LIST.value) { + if ( + getResourceTypeValueFor((obj as any).type) === PAGINATED_LIST.value + ) { return this.buildPaginatedList(obj, ...linksToFollow); } else if (isNotEmpty(linksToFollow)) { return [this.linkService.resolveLinks(obj, ...linksToFollow)]; @@ -107,9 +139,17 @@ export class RemoteDataBuildService { * @param entry the request entry the object has to match * @private */ - private hasExactMatchInObjectCache(href: string, entry: RequestEntry): boolean { - return hasValue(entry) && hasValue(entry.request) && isNotEmpty(entry.request.uuid) && - hasValue(href) && this.objectCache.hasByHref(href, entry.request.uuid); + private hasExactMatchInObjectCache( + href: string, + entry: RequestEntry + ): boolean { + return ( + hasValue(entry) && + hasValue(entry.request) && + isNotEmpty(entry.request.uuid) && + hasValue(href) && + this.objectCache.hasByHref(href, entry.request.uuid) + ); } /** @@ -118,7 +158,10 @@ export class RemoteDataBuildService { * @private */ private isCacheablePayload(entry: RequestEntry): boolean { - return hasValue(entry.response.payloadLink) && isNotEmpty(entry.response.payloadLink.href); + return ( + hasValue(entry.response.payloadLink) && + isNotEmpty(entry.response.payloadLink.href) + ); } /** @@ -138,26 +181,40 @@ export class RemoteDataBuildService { * @param object A plain object to be turned in to a {@link PaginatedList} * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - private buildPaginatedList(object: any, ...linksToFollow: FollowLinkConfig[]): Observable { - const pageLink = linksToFollow.find((linkToFollow: FollowLinkConfig) => linkToFollow.name === 'page'); - const otherLinks = linksToFollow.filter((linkToFollow: FollowLinkConfig) => linkToFollow.name !== 'page'); + private buildPaginatedList( + object: any, + ...linksToFollow: FollowLinkConfig[] + ): Observable { + const pageLink = linksToFollow.find( + (linkToFollow: FollowLinkConfig) => linkToFollow.name === 'page' + ); + const otherLinks = linksToFollow.filter( + (linkToFollow: FollowLinkConfig) => linkToFollow.name !== 'page' + ); const paginatedList = Object.assign(new PaginatedList(), object); if (hasValue(pageLink)) { if (isEmpty(paginatedList.page)) { - const pageSelfLinks = paginatedList._links.page.map((link: HALLink) => link.href); - return this.objectCache.getList(pageSelfLinks).pipe(map((page: any[]) => { - paginatedList.page = page - .map((obj: any) => this.plainObjectToInstance(obj)) - .map((obj: any) => - this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) - ); - if (isNotEmpty(otherLinks)) { - return this.linkService.resolveLinks(paginatedList, ...otherLinks); - } - return paginatedList; - })); + const pageSelfLinks = paginatedList._links.page.map( + (link: HALLink) => link.href + ); + return this.objectCache.getList(pageSelfLinks).pipe( + map((page: any[]) => { + paginatedList.page = page + .map((obj: any) => this.plainObjectToInstance(obj)) + .map((obj: any) => + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + ); + if (isNotEmpty(otherLinks)) { + return this.linkService.resolveLinks( + paginatedList, + ...otherLinks + ); + } + return paginatedList; + }) + ); } else { // in case the elements of the paginated list were already filled in, because they're UnCacheableObjects paginatedList.page = paginatedList.page @@ -166,7 +223,9 @@ export class RemoteDataBuildService { this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) ); if (isNotEmpty(otherLinks)) { - return observableOf(this.linkService.resolveLinks(paginatedList, ...otherLinks)); + return observableOf( + this.linkService.resolveLinks(paginatedList, ...otherLinks) + ); } } } @@ -179,13 +238,22 @@ export class RemoteDataBuildService { * @param requestUUID$ The UUID of the request we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildFromRequestUUID(requestUUID$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable> { + buildFromRequestUUID( + requestUUID$: string | Observable, + ...linksToFollow: FollowLinkConfig[] + ): Observable> { if (typeof requestUUID$ === 'string') { requestUUID$ = observableOf(requestUUID$); } - const requestEntry$ = requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)); + const requestEntry$ = requestUUID$.pipe( + getRequestFromRequestUUID(this.requestService) + ); - const payload$ = this.buildPayload(requestEntry$, undefined, ...linksToFollow); + const payload$ = this.buildPayload( + requestEntry$, + undefined, + ...linksToFollow + ); return this.toRemoteDataObservable(requestEntry$, payload$); } @@ -239,7 +307,10 @@ export class RemoteDataBuildService { * @param href$ self link of object we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildFromHref(href$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable> { + buildFromHref( + href$: string | Observable, + ...linksToFollow: FollowLinkConfig[] + ): Observable> { if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -248,7 +319,8 @@ export class RemoteDataBuildService { const requestUUID$ = href$.pipe( switchMap((href: string) => - this.objectCache.getRequestUUIDBySelfLink(href)), + this.objectCache.getRequestUUIDBySelfLink(href) + ) ); const requestEntry$ = observableCombineLatest([ @@ -275,7 +347,11 @@ export class RemoteDataBuildService { distinctUntilKeyChanged('lastUpdated') ); - const payload$ = this.buildPayload(requestEntry$, href$, ...linksToFollow); + const payload$ = this.buildPayload( + requestEntry$, + href$, + ...linksToFollow + ); return this.toRemoteDataObservable(requestEntry$, payload$); } @@ -286,19 +362,23 @@ export class RemoteDataBuildService { * @param href$ Observable href of object we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildSingle(href$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable> { + buildSingle( + href$: string | Observable, + ...linksToFollow: FollowLinkConfig[] + ): Observable> { return this.buildFromHref(href$, ...linksToFollow); } - toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { - return observableCombineLatest([ - requestEntry$, - payload$ - ]).pipe( - filter(([entry,payload]: [RequestEntry, T]) => - hasValue(entry) && - // filter out cases where the state is successful, but the payload isn't yet set - !(hasSucceeded(entry.state) && isUndefined(payload)) + toRemoteDataObservable( + requestEntry$: Observable, + payload$: Observable + ) { + return observableCombineLatest([requestEntry$, payload$]).pipe( + filter( + ([entry, payload]: [RequestEntry, T]) => + hasValue(entry) && + // filter out cases where the state is successful, but the payload isn't yet set + !(hasSucceeded(entry.state) && isUndefined(payload)) ), map(([entry, payload]: [RequestEntry, T]) => { let response = entry.response; @@ -328,8 +408,14 @@ export class RemoteDataBuildService { * @param href$ Observable href of objects we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - buildList(href$: string | Observable, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.buildFromHref>(href$, followLink('page', { shouldEmbed: false }, ...linksToFollow)); + buildList( + href$: string | Observable, + ...linksToFollow: FollowLinkConfig[] + ): Observable>> { + return this.buildFromHref>( + href$, + followLink('page', { shouldEmbed: false }, ...linksToFollow) + ); } /** @@ -342,8 +428,9 @@ export class RemoteDataBuildService { * * @param input the array of RemoteData observables to start from */ - aggregate(input: Observable>[]): Observable> { - + aggregate( + input: Observable>[] + ): Observable> { if (isEmpty(input)) { return createSuccessfulRemoteDataObject$([], new Date().getTime()); } @@ -352,15 +439,21 @@ export class RemoteDataBuildService { map((arr) => { const timeCompleted = arr .map((d: RemoteData) => d.timeCompleted) - .reduce((max: number, current: number) => current > max ? current : max); + .reduce((max: number, current: number) => + current > max ? current : max + ); const msToLive = arr .map((d: RemoteData) => d.msToLive) - .reduce((min: number, current: number) => current < min ? current : min); + .reduce((min: number, current: number) => + current < min ? current : min + ); const lastUpdated = arr .map((d: RemoteData) => d.lastUpdated) - .reduce((max: number, current: number) => current > max ? current : max); + .reduce((max: number, current: number) => + current > max ? current : max + ); let state: RequestEntryState; if (arr.some((d: RemoteData) => d.isRequestPending)) { @@ -383,11 +476,13 @@ export class RemoteDataBuildService { if (hasValue(e)) { return `[${idx}]: ${e}`; } - }).filter((e: string) => hasValue(e)) + }) + .filter((e: string) => hasValue(e)) .join(', '); - const statusCodes = new Set(arr - .map((d: RemoteData) => d.statusCode)); + const statusCodes = new Set( + arr.map((d: RemoteData) => d.statusCode) + ); let statusCode: number; @@ -408,6 +503,7 @@ export class RemoteDataBuildService { payload, statusCode ); - })); + }) + ); } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index e9dfbe7e2c3..de545623462 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, of } from 'rxjs'; import { MetadataRegistryCancelFieldAction, MetadataRegistryCancelSchemaAction, @@ -14,7 +14,7 @@ import { MetadataRegistryEditFieldAction, MetadataRegistryEditSchemaAction, MetadataRegistrySelectFieldAction, - MetadataRegistrySelectSchemaAction + MetadataRegistrySelectSchemaAction, } from '../../admin/admin-registries/metadata-registry/metadata-registry.actions'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { StoreMock } from '../../shared/testing/store.mock'; @@ -25,21 +25,25 @@ import { RegistryService } from './registry.service'; import { storeModuleConfig } from '../../app.reducer'; import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; import { MetadataFieldDataService } from '../data/metadata-field-data.service'; -import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + createNoContentRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { RemoteData } from '../data/remote-data'; import { NoContent } from '../shared/NoContent.model'; import { FindListOptions } from '../data/find-list-options.model'; +import { MetadataBitstreamDataService } from '../data/metadata-bitstream-data.service'; @Component({ template: '' }) -class DummyComponent { -} +class DummyComponent {} describe('RegistryService', () => { let registryService: RegistryService; let mockStore; let metadataSchemaService: MetadataSchemaDataService; let metadataFieldService: MetadataFieldDataService; + let metadataBitstreamDataService: MetadataBitstreamDataService; let options: FindListOptions; let mockSchemasList: MetadataSchema[]; @@ -48,113 +52,144 @@ describe('RegistryService', () => { function init() { options = Object.assign(new FindListOptions(), { currentPage: 1, - elementsPerPage: 20 + elementsPerPage: 20, }); mockSchemasList = [ Object.assign(new MetadataSchema(), { id: 1, _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1' } + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1', + }, }, prefix: 'dc', namespace: 'http://dublincore.org/documents/dcmi-terms/', - type: MetadataSchema.type + type: MetadataSchema.type, }), Object.assign(new MetadataSchema(), { id: 2, _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2' } + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2', + }, }, prefix: 'mock', namespace: 'http://dspace.org/mockschema', - type: MetadataSchema.type - }) + type: MetadataSchema.type, + }), ]; mockFieldsList = [ - Object.assign(new MetadataField(), - { - id: 1, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8' } + Object.assign(new MetadataField(), { + id: 1, + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8', }, - element: 'contributor', - qualifier: 'advisor', - scopeNote: null, - schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), - type: MetadataField.type - }), - Object.assign(new MetadataField(), - { - id: 2, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9' } + }, + element: 'contributor', + qualifier: 'advisor', + scopeNote: null, + schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), + type: MetadataField.type, + }), + Object.assign(new MetadataField(), { + id: 2, + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9', }, - element: 'contributor', - qualifier: 'author', - scopeNote: null, - schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), - type: MetadataField.type - }), - Object.assign(new MetadataField(), - { - id: 3, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10' } + }, + element: 'contributor', + qualifier: 'author', + scopeNote: null, + schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), + type: MetadataField.type, + }), + Object.assign(new MetadataField(), { + id: 3, + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10', }, - element: 'contributor', - qualifier: 'editor', - scopeNote: 'test scope note', - schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), - type: MetadataField.type - }), - Object.assign(new MetadataField(), - { - id: 4, - _links: { - self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11' } + }, + element: 'contributor', + qualifier: 'editor', + scopeNote: 'test scope note', + schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), + type: MetadataField.type, + }), + Object.assign(new MetadataField(), { + id: 4, + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11', }, - element: 'contributor', - qualifier: 'illustrator', - scopeNote: null, - schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), - type: MetadataField.type - }) + }, + element: 'contributor', + qualifier: 'illustrator', + scopeNote: null, + schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), + type: MetadataField.type, + }), ]; metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)), + findAll: createSuccessfulRemoteDataObject$( + createPaginatedList(mockSchemasList) + ), findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]), - createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), + createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$( + mockSchemasList[0] + ), delete: createNoContentRemoteDataObject$(), - clearRequests: observableOf('href') + clearRequests: observableOf('href'), }); metadataFieldService = jasmine.createSpyObj('metadataFieldService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)), + findAll: createSuccessfulRemoteDataObject$( + createPaginatedList(mockFieldsList) + ), findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]), create: createSuccessfulRemoteDataObject$(mockFieldsList[0]), put: createSuccessfulRemoteDataObject$(mockFieldsList[0]), delete: createNoContentRemoteDataObject$(), - clearRequests: observableOf('href') + clearRequests: observableOf('href'), }); + metadataBitstreamDataService = jasmine.createSpyObj( + 'metadataBitstreamDataService', + { + searchByHandleParams: of({ + /* Your Mock Data */ + }), + } + ); } beforeEach(() => { init(); TestBed.configureTestingModule({ - imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()], - declarations: [ - DummyComponent + imports: [ + CommonModule, + StoreModule.forRoot({}, storeModuleConfig), + TranslateModule.forRoot(), ], + declarations: [DummyComponent], providers: [ { provide: Store, useClass: StoreMock }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { + provide: NotificationsService, + useValue: new NotificationsServiceStub(), + }, { provide: MetadataSchemaDataService, useValue: metadataSchemaService }, { provide: MetadataFieldDataService, useValue: metadataFieldService }, - RegistryService - ] + { + provide: MetadataBitstreamDataService, + useValue: metadataBitstreamDataService, + }, + RegistryService, + ], }); registryService = TestBed.inject(RegistryService); mockStore = TestBed.inject(Store); @@ -179,12 +214,18 @@ describe('RegistryService', () => { let result; beforeEach(() => { - result = registryService.getMetadataSchemaByPrefix(mockSchemasList[0].prefix); + result = registryService.getMetadataSchemaByPrefix( + mockSchemasList[0].prefix + ); }); it('should call metadataSchemaService.findById with the correct ID', (done) => { result.subscribe(() => { - expect(metadataSchemaService.findById).toHaveBeenCalledWith(`${mockSchemasList[0].id}`, true, true); + expect(metadataSchemaService.findById).toHaveBeenCalledWith( + `${mockSchemasList[0].id}`, + true, + true + ); done(); }); }); @@ -201,7 +242,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryEditSchemaAction(mockSchemasList[0]) + ); }); }); @@ -211,7 +254,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryCancelSchemaAction', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction()); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryCancelSchemaAction() + ); }); }); @@ -221,7 +266,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistrySelectSchemaAction(mockSchemasList[0]) + ); }); }); @@ -231,7 +278,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryDeselectSchemaAction(mockSchemasList[0]) + ); }); }); @@ -241,7 +290,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction()); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryDeselectAllSchemaAction() + ); }); }); @@ -251,7 +302,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryEditFieldAction(mockFieldsList[0]) + ); }); }); @@ -261,7 +314,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryCancelFieldAction', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction()); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryCancelFieldAction() + ); }); }); @@ -271,7 +326,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistrySelectFieldAction(mockFieldsList[0]) + ); }); }); @@ -281,7 +338,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0])); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryDeselectFieldAction(mockFieldsList[0]) + ); }); }); @@ -291,7 +350,9 @@ describe('RegistryService', () => { }); it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => { - expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction()); + expect(mockStore.dispatch).toHaveBeenCalledWith( + new MetadataRegistryDeselectAllFieldAction() + ); }); }); }); @@ -315,7 +376,10 @@ describe('RegistryService', () => { let result: Observable; beforeEach(() => { - result = registryService.createMetadataField(mockFieldsList[0], mockSchemasList[0]); + result = registryService.createMetadataField( + mockFieldsList[0], + mockSchemasList[0] + ); }); it('should return the created metadata field', (done) => { @@ -333,7 +397,10 @@ describe('RegistryService', () => { beforeEach(() => { metadataField = mockFieldsList[0]; metadataField.qualifier = ''; - result = registryService.createMetadataField(metadataField, mockSchemasList[0]); + result = registryService.createMetadataField( + metadataField, + mockSchemasList[0] + ); }); it('should return the created metadata field with a null qualifier', (done) => { diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 566533ae65e..b91eb183d98 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -2,7 +2,11 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list.model'; -import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; +import { + hasValue, + hasValueOperator, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; diff --git a/src/app/core/shared/clarin/constants.ts b/src/app/core/shared/clarin/constants.ts index 5dc8e9be50f..45ef74a9ed8 100644 --- a/src/app/core/shared/clarin/constants.ts +++ b/src/app/core/shared/clarin/constants.ts @@ -6,4 +6,5 @@ export const DOWNLOAD_TOKEN_EXPIRED_EXCEPTION = 'DownloadTokenExpiredException'; export const HTTP_STATUS_UNAUTHORIZED = 401; export const USER_WITHOUT_EMAIL_EXCEPTION = 'UserWithoutEmailException'; export const MISSING_HEADERS_FROM_IDP_EXCEPTION = 'MissingHeadersFromIpd'; +export const BASE_LOCAL_URL = 'http://localhost:8080'; diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 66c6488b8e4..053d7321c84 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; import { ItemDataService } from '../../core/data/item-data.service'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { TruncatePipe } from '../../shared/utils/truncate.pipe'; @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import {BehaviorSubject, of, of as observableOf} from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -20,6 +20,13 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; import { RemoteData } from '../../core/data/remote-data'; +import { RegistryService } from 'src/app/core/registry/registry.service'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; +import { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service'; +import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-data.service'; +import { MetadataBitstreamDataService } from 'src/app/core/data/metadata-bitstream-data.service'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -50,14 +57,13 @@ const metadataServiceStub = { describe('FullItemPageComponent', () => { let comp: FullItemPageComponent; let fixture: ComponentFixture; - + let registryService: RegistryService; + let translateService: TranslateService; let authService: AuthService; let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; - - beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), @@ -76,6 +82,11 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); + const mockMetadataBitstreamDataService = { + searchByHandleParams: () => of({}) // Returns a mock Observable + }; + + translateService = getMockTranslateService(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -90,6 +101,12 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: MetadataBitstreamDataService, useValue: mockMetadataBitstreamDataService }, + { provide: Store, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: MetadataSchemaDataService, useValue: {} }, + { provide: MetadataFieldDataService, useValue: {} }, + RegistryService ], schemas: [NO_ERRORS_SCHEMA] @@ -99,6 +116,7 @@ describe('FullItemPageComponent', () => { })); beforeEach(waitForAsync(() => { + registryService = TestBed.inject(RegistryService); fixture = TestBed.createComponent(FullItemPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 118e4360048..63e29592272 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -16,6 +16,7 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { RegistryService } from 'src/app/core/registry/registry.service'; /** @@ -48,8 +49,9 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, items: ItemDataService, authService: AuthService, authorizationService: AuthorizationDataService, + protected registryService: RegistryService, private _location: Location) { - super(route, router, items, authService, authorizationService); + super(route, router, items, authService, authorizationService, registryService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 3f0350d3629..1c6174bca18 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -19,7 +19,6 @@ import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component'; -import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component'; import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component'; import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; @@ -71,10 +70,14 @@ import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/ import { FileSectionComponent } from './simple/field-components/file-section/file-section.component'; import { ItemSharedModule } from './item-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; - -import { FileDescriptionComponent } from './simple/field-components/preview-section/file-description/file-description.component'; -import { FileTreeViewComponent } from './simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component'; -import { PreviewSectionComponent } from './simple/field-components/preview-section/preview-section.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import {PreviewSectionComponent} from './simple/field-components/preview-section/preview-section.component'; +import { + FileDescriptionComponent +} from './simple/field-components/preview-section/file-description/file-description.component'; +import { + FileTreeViewComponent +} from './simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -148,15 +151,11 @@ const DECLARATIONS = [ ResultsBackButtonModule, UploadModule, DsoPageModule, - ChartsModule + ChartsModule, + NgbModule ], - declarations: [ - ...DECLARATIONS, - - ], - exports: [ - ...DECLARATIONS - ] + declarations: [...DECLARATIONS], + exports: [...DECLARATIONS], }) export class ItemPageModule { /** @@ -166,8 +165,7 @@ export class ItemPageModule { static withEntryComponents() { return { ngModule: ItemPageModule, - providers: ENTRY_COMPONENTS.map((component) => ({provide: component})) + providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })), }; } - } diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html index 8a5cd15c580..0fb29830901 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html @@ -1,9 +1,12 @@
-
+
+ + Your browser does not support the video tag. +
@@ -32,25 +35,27 @@
Preview
- +  Download file @@ -58,7 +63,7 @@
@@ -71,18 +76,31 @@ class="pull-right collapsed" data-toggle="collapse" role="button" - href="#file_file_{{fileInput.id}}" + href="#file_file_{{ fileInput.id }}" >  
    - - + + -
    {{fileInput.fileInfo[0].content}}
    +
    {{ fileInput.fileInfo[0].content }}
    +
    + +
diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.scss b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.scss index 2336ee20794..005bd540250 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.scss +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.scss @@ -38,16 +38,19 @@ margin-left: 180px; } } - + .preview-image { width: 10%; height: 10%; } - + } .button-container { + a { + text-decoration: none; + } .download-btn, .preview-btn { display: inline; padding: .2em .6em .3em; @@ -63,6 +66,10 @@ cursor: pointer; background-color: #5bc0de; } + + .download-btn:hover, .preview-btn:hover { + background-color: #31b0d5; + } } .panel { @@ -71,11 +78,11 @@ border-radius: 4px; box-shadow: 0 1px 1px rgba(0,0,0,0.05); } - + .panel-info { border-color: #bce8f1; } - + .panel-info>.panel-heading { color: #3a87ad; background-color: #d9edf7; @@ -103,4 +110,4 @@ .pull-right { float: right !important; } -} \ No newline at end of file +} diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts index 68145d2fff4..b893b1f4346 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; +import { BASE_LOCAL_URL } from 'src/app/core/shared/clarin/constants'; @Component({ selector: 'ds-file-description', @@ -13,4 +14,7 @@ export class FileDescriptionComponent implements OnInit { ngOnInit(): void { console.log(this.fileInput); } + public downloadFiles() { + window.location.href = `${BASE_LOCAL_URL}${this.fileInput.href}`; + } } diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.html b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.html index 6644016bb99..9968164d377 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.html +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.html @@ -1,6 +1,9 @@
  • - - + + {{ node.name }} @@ -12,7 +15,8 @@
      @@ -21,4 +25,4 @@ [node]="node.sub[subNodeKey]" >
    -
  • \ No newline at end of file + diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.scss b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.scss index 1aaef2bf84c..0585ae22f29 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.scss +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.scss @@ -20,4 +20,12 @@ .pull-right { float: right !important; -} \ No newline at end of file +} + +.foldername a { + cursor: pointer; +} + +.foldername a:hover { + text-decoration: underline; +} diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.ts b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.ts index 4471ebb25b7..96129517885 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { FileInfo } from 'src/app/core/metadata/metadata-bitstream.model'; @Component({ @@ -6,15 +6,13 @@ import { FileInfo } from 'src/app/core/metadata/metadata-bitstream.model'; templateUrl: './file-tree-view.component.html', styleUrls: ['./file-tree-view.component.scss'], }) -export class FileTreeViewComponent implements OnInit { +export class FileTreeViewComponent { @Input() node: FileInfo; + isCollapsed = false; + getKeys(obj: any): string[] { return Object.keys(obj); } - - ngOnInit(): void { - console.log(this.node); - } } diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts index e88b994e9f0..8258c340d86 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts @@ -6,6 +6,7 @@ import { RegistryService } from 'src/app/core/registry/registry.service'; import { PreviewSectionComponent } from './preview-section.component'; import { ResourceType } from 'src/app/core/shared/resource-type'; import { HALLink } from 'src/app/core/shared/hal-link.model'; +import { Item } from 'src/app/core/shared/item.model'; describe('PreviewSectionComponent', () => { let component: PreviewSectionComponent; @@ -51,6 +52,9 @@ describe('PreviewSectionComponent', () => { of(bitstreamStream) ); + component.item = new Item(); + component.item.handle = '12345'; + fixture.detectChanges(); }); diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts index a515c30306b..24e9dcc5580 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts @@ -21,7 +21,7 @@ export class PreviewSectionComponent implements OnInit { ngOnInit(): void { this.registryService - .getMetadataBitstream('123456789/36', 'ORIGINAL,TEXT,THUMBNAIL') + .getMetadataBitstream(this.item.handle, 'ORIGINAL,TEXT,THUMBNAIL') .pipe(getAllSucceededRemoteListPayload()) .subscribe((data: MetadataBitstream[]) => { this.listOfFiles.next(data); diff --git a/src/app/item-page/simple/item-page.component.scss b/src/app/item-page/simple/item-page.component.scss index 24ab7394c3e..5e419c675d0 100644 --- a/src/app/item-page/simple/item-page.component.scss +++ b/src/app/item-page/simple/item-page.component.scss @@ -28,3 +28,58 @@ margin-top: -16px; padding-top: 16px; } + +.btn-download{ + color: #fff !important; + background-color: #428bca; + border-color: #357ebd; + cursor: pointer; +} + +.btn-download:hover { + color: #fff; + background-color: #3276b1; + border-color: #285e8e; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.428571429; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + white-space: pre-wrap; +} + +#command-div .repo-copy-btn { + opacity: 0; + -webkit-transition: opacity .3s ease-in-out; + -o-transition: opacity .3s ease-in-out; + transition: opacity .3s ease-in-out; +} + +.repo-copy-btn { + width: 20px; + height: 20px; + position: relative; + display: inline-block; + padding: 0 !important; +} + +.repo-copy-btn:before { + content: " "; + background-image: url('https://lindat.mff.cuni.cz/repository/xmlui/themes/UFAL/images/clippy.svg'); + background-size: 13px 15px; + background-repeat: no-repeat; + background-color: red; + display: inline-block; + width: 13px; + height: 15px; + padding: 0 !important; +} diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 9b0e87939df..7db6cd7148a 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { ItemDataService } from '../../core/data/item-data.service'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; @@ -12,21 +12,28 @@ import { Item } from '../../core/shared/item.model'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createRelationshipsObservable } from './item-types/shared/item.component.spec'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, of } from 'rxjs'; import { createFailedRemoteDataObject$, createPendingRemoteDataObject$, createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$ + createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { RegistryService } from 'src/app/core/registry/registry.service'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; +import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-data.service'; +import { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service'; +import { MetadataBitstreamDataService } from 'src/app/core/data/metadata-bitstream-data.service'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], - relationships: createRelationshipsObservable() + relationships: createRelationshipsObservable(), }); const mockWithdrawnItem: Item = Object.assign(new Item(), { @@ -40,6 +47,11 @@ describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; let authService: AuthService; + let translateService: TranslateService; + let registryService: RegistryService; + const authorizationService = jasmine.createSpyObj('authorizationService', [ + 'isAuthorized', + ]); let authorizationDataService: AuthorizationDataService; const mockMetadataService = { @@ -49,25 +61,34 @@ describe('ItemPageComponent', () => { /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ }; const mockRoute = Object.assign(new ActivatedRouteStub(), { - data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }) + data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) }), }); + const mockMetadataBitstreamDataService = { + searchByHandleParams: () => of({}) // Returns a mock Observable + }; + beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), - setRedirectUrl: {} + setRedirectUrl: {}, }); + + translateService = getMockTranslateService(); authorizationDataService = jasmine.createSpyObj('authorizationDataService', { isAuthorized: observableOf(false), }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), BrowserAnimationsModule], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + BrowserAnimationsModule, + ], declarations: [ItemPageComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: mockRoute }, @@ -75,16 +96,26 @@ describe('ItemPageComponent', () => { { provide: MetadataService, useValue: mockMetadataService }, { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Store, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: MetadataSchemaDataService, useValue: {} }, + { provide: MetadataFieldDataService, useValue: {} }, + { provide: MetadataBitstreamDataService, useValue: mockMetadataBitstreamDataService }, + RegistryService, { provide: AuthorizationDataService, useValue: authorizationDataService }, ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideComponent(ItemPageComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default } - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ItemPageComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); })); beforeEach(waitForAsync(() => { + registryService = TestBed.inject(RegistryService); fixture = TestBed.createComponent(ItemPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index a4f9eb3766c..d52ed7c410e 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,13 +1,14 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; - -import { Observable} from 'rxjs'; import { map, take } from 'rxjs/operators'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { fadeInOut } from '../../shared/animations/fade'; -import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getAllSucceededRemoteListPayload, +} from '../../core/shared/operators'; import { ViewMode } from '../../core/shared/view-mode.model'; import { AuthService } from '../../core/auth/auth.service'; import { getItemPageRoute } from '../item-page-routing-paths'; @@ -15,6 +16,10 @@ import { isNotEmpty } from '../../shared/empty.util'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { RegistryService } from 'src/app/core/registry/registry.service'; +import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; +import { BASE_LOCAL_URL } from 'src/app/core/shared/clarin/constants'; +import { Observable } from 'rxjs'; /** * This component renders a simple item page. @@ -26,10 +31,9 @@ import { redirectOn4xx } from '../../core/shared/authorized.operators'; styleUrls: ['./item-page.component.scss'], templateUrl: './item-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeInOut] + animations: [fadeInOut], }) export class ItemPageComponent implements OnInit { - /** * The item's id */ @@ -49,6 +53,22 @@ export class ItemPageComponent implements OnInit { * Route to the item's page */ itemPageRoute$: Observable; + /** + * handle of the specific item + */ + itemHandle: string; + /** + * handle of the specific item + */ + fileName: string; + /** + * determine to show download all zip button or not + */ + canDownloadAllFiles = true; + /** + * command for the download command feature + */ + command: string; /** * Whether the current user is an admin or not @@ -65,6 +85,19 @@ export class ItemPageComponent implements OnInit { */ withdrawnTombstone = false; + /** + * If download by command button is click, the command line will be shown + */ + isCommandLineVisible = false; + /** + * list of files uploaded by users to this item + */ + listOfFiles: MetadataBitstream[]; + /** + * total size of list of files uploaded by users to this item + */ + totalFileSizes: string; + itemUrl: string; constructor( @@ -72,9 +105,9 @@ export class ItemPageComponent implements OnInit { private router: Router, private items: ItemDataService, private authService: AuthService, - private authorizationService: AuthorizationDataService - ) { - } + private authorizationService: AuthorizationDataService, + protected registryService: RegistryService + ) {} /** * Initialize instance variables @@ -90,6 +123,43 @@ export class ItemPageComponent implements OnInit { ); this.showTombstone(); + + this.registryService + .getMetadataBitstream(this.itemHandle, 'ORIGINAL,TEXT,THUMBNAIL') + .pipe(getAllSucceededRemoteListPayload()) + .subscribe((data: MetadataBitstream[]) => { + this.listOfFiles = data; + this.generateCurlCommand(); + this.sumFileSizes(); + }); + } + + sumFileSizes() { + const sizeUnits = { + B: 1, + KB: 1000, + MB: 1000 * 1000, + GB: 1000 * 1000 * 1000, + TB: 1000 * 1000 * 1000 * 1000, + }; + + let totalBytes = this.listOfFiles.reduce((total, file) => { + const [valueStr, unit] = file.fileSize.split(' '); + const value = parseFloat(valueStr); + const bytes = value * sizeUnits[unit.toUpperCase()]; + return total + bytes; + }, 0); + + let finalUnit = 'B'; + for (const unit of ['KB', 'MB', 'GB', 'TB']) { + if (totalBytes < 1000) { + break; + } + totalBytes /= 1000; + finalUnit = unit; + } + + this.totalFileSizes = totalBytes.toFixed(2) + ' ' + finalUnit; } showTombstone() { @@ -99,10 +169,10 @@ export class ItemPageComponent implements OnInit { let isReplaced = ''; // load values from item - this.itemRD$.pipe( - take(1), - getAllSucceededRemoteDataPayload()) + this.itemRD$ + .pipe(take(1), getAllSucceededRemoteDataPayload()) .subscribe((item: Item) => { + this.itemHandle = item.handle; isWithdrawn = item.isWithdrawn; isReplaced = item.metadata['dc.relation.isreplacedby']?.[0]?.value; }); @@ -114,8 +184,10 @@ export class ItemPageComponent implements OnInit { // for users navigate to the custom tombstone // for admin stay on the item page with tombstone flag - this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); - this.isAdmin$.subscribe(isAdmin => { + this.isAdmin$ = this.authorizationService.isAuthorized( + FeatureID.AdministratorOf + ); + this.isAdmin$.subscribe((isAdmin) => { // do not show tombstone for admin but show it for users if (!isAdmin) { if (isNotEmpty(isReplaced)) { @@ -126,4 +198,26 @@ export class ItemPageComponent implements OnInit { } }); } + + setCommandline() { + this.isCommandLineVisible = !this.isCommandLineVisible; + } + + generateCurlCommand() { + const fileNames = this.listOfFiles.map((file: MetadataBitstream) => { + if (!file.canPreview) { + this.canDownloadAllFiles = file.canPreview; + } + + return file.name; + }); + + this.command = `curl --remote-name-all ${BASE_LOCAL_URL}/server/bitstream/handle/${ + this.itemHandle + }/{${fileNames.join(',')}}`; + } + + downloadFiles() { + window.location.href = `${BASE_LOCAL_URL}/server/bitstream/allzip?handleId=${this.itemHandle}`; + } }