diff --git a/README.md b/README.md index de97be31ce72f77e3cb4b81a5ad9700e968355e2..d979af0d4c9d165a1fda96d2db4d602a846d5769 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ dspace-angular This project is currently in pre-alpha. -You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular). +You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular). If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype) diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index ee7b101f969dcefefc9ebaaacc5d2bd0c75a0457..90ea2026e3190eaf30af6597898dee3e49a570ff 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -12,8 +12,8 @@ describe('protractor App', function() { expect(page.getPageTitleText()).toEqual('DSpace'); }); - it('should display title "Hello, World!"', () => { + it('should display header "Welcome to DSpace"', () => { page.navigateTo(); - expect(page.getFirstPText()).toEqual('Hello, World!'); + expect(page.getFirstHeaderText()).toEqual('Welcome to DSpace'); }); }); diff --git a/e2e/app.po.ts b/e2e/app.po.ts index 164c524620b30221f43b22e6f106b5bbc49d4b2f..d8d2acf120ed7efd027ef16dc6900b1d34bc192d 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -12,4 +12,8 @@ export class ProtractorPage { getFirstPText() { return element(by.xpath('//p[1]')).getText(); } -} \ No newline at end of file + + getFirstHeaderText() { + return element(by.xpath('//h1[1]')).getText(); + } +} diff --git a/package.json b/package.json index 316b12f0b6b0a0b272f9c76e78c9abf7ad440bcc..8dd1110d1a9f1c54690f870b54d1a7e20d71ee48 100644 --- a/package.json +++ b/package.json @@ -78,19 +78,21 @@ "@angular/upgrade": "2.2.3", "@angularclass/bootloader": "1.0.1", "@angularclass/idle-preload": "1.0.4", - "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.24", + "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.15", "@ngrx/core": "^1.2.0", "@ngrx/effects": "^2.0.0", "@ngrx/router-store": "^1.2.5", "@ngrx/store": "^2.2.1", "@ngrx/store-devtools": "^3.2.2", + "@ngx-translate/core": "^6.0.1", + "@ngx-translate/http-loader": "^0.0.3", "@types/jsonschema": "0.0.5", "angular2-express-engine": "2.1.0-rc.1", "angular2-platform-node": "2.1.0-rc.1", "angular2-universal": "2.1.0-rc.1", "angular2-universal-polyfills": "2.1.0-rc.1", "body-parser": "1.15.2", - "bootstrap": "4.0.0-alpha.6", + "bootstrap": "4.0.0-alpha.5", "cerialize": "^0.1.13", "compression": "1.6.2", "express": "4.14.0", @@ -100,9 +102,8 @@ "jsonschema": "^1.1.1", "methods": "1.1.2", "morgan": "1.7.0", - "ng2-pagination": "^2.0.0", - "ng2-translate": "4.2.0", "preboot": "4.5.2", + "reflect-metadata": "^0.1.10", "rxjs": "5.0.0-beta.12", "ts-md5": "^1.2.0", "webfontloader": "1.6.27", @@ -160,7 +161,6 @@ "protractor": "~4.0.14", "protractor-istanbul-plugin": "~2.0.0", "raw-loader": "0.5.1", - "reflect-metadata": "0.1.8", "rimraf": "2.5.4", "rollup": "0.37.0", "rollup-plugin-commonjs": "6.0.0", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 80a68206eae6ad288d2b9f4c73d72ecb2d80bf12..95358dc446b006e90770b5e36772a73a02522cc6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -1,16 +1,21 @@ { - "example": { - "with": { - "data": "{{greeting}}, {{recipient}}!" - } - }, - "footer": { "copyright": "copyright © 2002-{{ year }}", "link.dspace": "DSpace software", "link.duraspace": "DuraSpace" }, + "item": { + "page": { + "author": "Author", + "abstract": "Abstract", + "date": "Date", + "uri": "URI", + "files": "Files", + "collections": "Collections" + } + }, + "nav": { "home": "Home" }, @@ -31,5 +36,12 @@ "link": { "home-page": "Take me to the home page" } + }, + + "home": { + "top-level-communities": { + "head": "Communities in DSpace", + "help": "Select a community to browse its collections." + } } } diff --git a/src/app/app.component.html b/src/app/app.component.html index a83530c27dc863eb30e2cc6f16c3c37767370506..a227b80ab04bcc10553a4e4c003cbb3681b776ab 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,25 +4,6 @@ <main class="main-content"> <div class="container-fluid"> - <p>{{ 'example.with.data' | translate:data }}</p> - <p>{{ example }}</p> - <h2 [ngClass]="{ 'red': EnvConfig.production, 'green': !EnvConfig.production }"> - <span *ngIf="!EnvConfig.production">development</span> - <span *ngIf="EnvConfig.production">production</span> - </h2> - - {{options.id}} - <!--ds-pagination [paginationOptions]="options" - [collectionSize]="100" - (pageChange)="options.currentPage = $event" - (pageSizeChange)="options.pageSize = $event"> - - <ul> - <li *ngFor="let item of collection | paginate: { itemsPerPage: options.pageSize, currentPage: options.currentPage, totalItems: 100 }"> {{item}} </li> - </ul> - - </ds-pagination--> - <router-outlet></router-outlet> </div> </main> diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 2a58ae0aa28750ad65333e3e958cb596893f944a..7b86523886b55de3cdc5bef7959bbe57735451d1 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -15,11 +15,3 @@ .main-content { flex: 1 0 auto; } - -h2.red { - color: red; -} - -h2.green { - color: green; -} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index d56be1a807943502e4e1286d6cdc812aecbe9883..9b4aa66bf64c5a788bb7c00d55979d736f4ef7a5 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -10,7 +10,7 @@ import { DebugElement } from "@angular/core"; import { By } from '@angular/platform-browser'; -import { TranslateModule, TranslateLoader } from "ng2-translate"; +import { TranslateModule, TranslateLoader } from "@ngx-translate/core"; import { Store, StoreModule } from "@ngrx/store"; // Load the implementations that should be tested @@ -34,8 +34,10 @@ describe('App component', () => { beforeEach(async(() => { return TestBed.configureTestingModule({ imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({ - provide: TranslateLoader, - useClass: MockTranslateLoader + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } })], declarations: [AppComponent], // declare the test component providers: [ @@ -52,8 +54,8 @@ describe('App component', () => { comp = fixture.componentInstance; // component test instance - // query for the title <p> by CSS element selector - de = fixture.debugElement.query(By.css('p')); + // query for the <div class="outer-wrapper"> by CSS element selector + de = fixture.debugElement.query(By.css('div.outer-wrapper')); el = de.nativeElement; }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 856f964283f6f8ccce0b6c0a9330344de378a683..1cf97e763cf4cb019a4773bbb1098acd6960b767 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,17 +3,14 @@ import { ChangeDetectionStrategy, Inject, ViewEncapsulation, - OnDestroy, OnInit, HostListener } from "@angular/core"; -import { TranslateService } from "ng2-translate"; +import { TranslateService } from "@ngx-translate/core"; import { HostWindowState } from "./shared/host-window.reducer"; import { Store } from "@ngrx/store"; import { HostWindowResizeAction } from "./shared/host-window.actions"; -import { PaginationOptions } from './core/shared/pagination-options.model'; - -import { GLOBAL_CONFIG, GlobalConfig } from '../config'; +import { EnvConfig, GLOBAL_CONFIG, GlobalConfig } from '../config'; @Component({ changeDetection: ChangeDetectionStrategy.Default, @@ -22,16 +19,7 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config'; templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent implements OnDestroy, OnInit { - private translateSubscription: any; - - collection = []; - example: string; - options: PaginationOptions = new PaginationOptions(); - data: any = { - greeting: 'Hello', - recipient: 'World' - }; +export class AppComponent implements OnInit { constructor( @Inject(GLOBAL_CONFIG) public EnvConfig: GlobalConfig, @@ -42,26 +30,12 @@ export class AppComponent implements OnDestroy, OnInit { translate.setDefaultLang('en'); // the lang to use, if the lang isn't available, it will use the current loader to get them translate.use('en'); - for (let i = 1; i <= 100; i++) { - this.collection.push(`item ${i}`); - } } ngOnInit() { - this.translateSubscription = this.translate.get('example.with.data', { greeting: 'Hello', recipient: 'DSpace' }).subscribe((translation: string) => { - this.example = translation; - }); - this.onLoad(); - this.options.id = 'app'; - //this.options.currentPage = 1; - this.options.pageSize = 15; - this.options.size = 'sm'; - } - - ngOnDestroy() { - if (this.translateSubscription) { - this.translateSubscription.unsubscribe(); - } + const env: string = EnvConfig.production ? "Production" : "Development"; + const color: string = EnvConfig.production ? "red" : "green"; + console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); } @HostListener('window:resize', ['$event']) @@ -71,9 +45,4 @@ export class AppComponent implements OnDestroy, OnInit { ); } - private onLoad() { - this.store.dispatch( - new HostWindowResizeAction(window.innerWidth, window.innerHeight) - ); - } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 920195a0f5f7a466ec89cee6b6115164082ab948..42304c865e94a4187e470c33d849d557ba3d7b88 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CoreModule } from './core/core.module'; import { HomeModule } from './home/home.module'; +import { ItemPageModule } from './item-page/item-page.module'; import { SharedModule } from './shared/shared.module'; @@ -10,15 +11,17 @@ import { AppComponent } from './app.component'; import { HeaderComponent } from './header/header.component'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; + @NgModule({ declarations: [ AppComponent, HeaderComponent, - PageNotFoundComponent + PageNotFoundComponent, ], imports: [ SharedModule, HomeModule, + ItemPageModule, CoreModule.forRoot(), AppRoutingModule ], diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts new file mode 100644 index 0000000000000000000000000000000000000000..00cb50663aa8ce8e039a6542a17ec8369a53276d --- /dev/null +++ b/src/app/core/cache/builders/build-decorators.ts @@ -0,0 +1,41 @@ +import "reflect-metadata"; +import { GenericConstructor } from "../../shared/generic-constructor"; +import { CacheableObject } from "../object-cache.reducer"; +import { NormalizedDSOType } from "../models/normalized-dspace-object-type"; + +const mapsToMetadataKey = Symbol("mapsTo"); +const relationshipKey = Symbol("relationship"); + +const relationshipMap = new Map(); + +export const mapsTo = function(value: GenericConstructor<CacheableObject>) { + return Reflect.metadata(mapsToMetadataKey, value); +}; + +export const getMapsTo = function(target: any) { + return Reflect.getOwnMetadata(mapsToMetadataKey, target); +}; + +export const relationship = function(value: NormalizedDSOType): any { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target || !propertyKey) { + return; + } + + let metaDataList : Array<string> = relationshipMap.get(target.constructor) || []; + if (metaDataList.indexOf(propertyKey) === -1) { + metaDataList.push(propertyKey); + } + relationshipMap.set(target.constructor, metaDataList); + + return Reflect.metadata(relationshipKey, value).apply(this, arguments); + }; +}; + +export const getResourceType = function(target: any, propertyKey: string) { + return Reflect.getMetadata(relationshipKey, target, propertyKey); +}; + +export const getRelationships = function(target: any) { + return relationshipMap.get(target); +}; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f37f0ce4f942def6b27b1d388fbcced25185b874 --- /dev/null +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -0,0 +1,156 @@ +import { Injectable } from "@angular/core"; +import { CacheableObject } from "../object-cache.reducer"; +import { ObjectCacheService } from "../object-cache.service"; +import { RequestService } from "../../data/request.service"; +import { ResponseCacheService } from "../response-cache.service"; +import { Store } from "@ngrx/store"; +import { CoreState } from "../../core.reducers"; +import { RequestEntry } from "../../data/request.reducer"; +import { hasValue, isNotEmpty } from "../../../shared/empty.util"; +import { ResponseCacheEntry } from "../response-cache.reducer"; +import { ErrorResponse, SuccessResponse } from "../response-cache.models"; +import { Observable } from "rxjs/Observable"; +import { RemoteData } from "../../data/remote-data"; +import { GenericConstructor } from "../../shared/generic-constructor"; +import { getMapsTo, getResourceType, getRelationships } from "./build-decorators"; +import { NormalizedDSOFactory } from "../models/normalized-dspace-object-factory"; + +@Injectable() +export class RemoteDataBuildService { + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store<CoreState>, + ) { + } + + buildSingle<TNormalized extends CacheableObject, TDomain>( + href: string, + normalizedType: GenericConstructor<TNormalized> + ): RemoteData<TDomain> { + const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href); + const responseCacheObs = this.responseCache.get(href); + + const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); + + const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); + + const isSuccessFul = responseCacheObs + .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); + + const errorMessage = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage) + .distinctUntilChanged(); + + const payload = + Observable.race( + this.objectCache.getBySelfLink<TNormalized>(href, normalizedType), + responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs) + .flatMap((resourceUUIDs: Array<string>) => { + if (isNotEmpty(resourceUUIDs)) { + return this.objectCache.get(resourceUUIDs[0], normalizedType); + } + else { + return Observable.of(undefined); + } + }) + .distinctUntilChanged() + ).map((normalized: TNormalized) => { + return this.build<TNormalized, TDomain>(normalized); + }); + + return new RemoteData( + href, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + payload + ); + } + + buildList<TNormalized extends CacheableObject, TDomain>( + href: string, + normalizedType: GenericConstructor<TNormalized> + ): RemoteData<TDomain[]> { + const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href); + const responseCacheObs = this.responseCache.get(href); + + const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); + + const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); + + const isSuccessFul = responseCacheObs + .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); + + const errorMessage = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage) + .distinctUntilChanged(); + + const payload = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs) + .flatMap((resourceUUIDs: Array<string>) => { + return this.objectCache.getList(resourceUUIDs, normalizedType) + .map((normList: TNormalized[]) => { + return normList.map((normalized: TNormalized) => { + return this.build<TNormalized, TDomain>(normalized); + }); + }); + }) + .distinctUntilChanged(); + + return new RemoteData( + href, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + payload + ); + } + + + build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain { + let links: any = {}; + + const relationships = getRelationships(normalized.constructor) || []; + + relationships.forEach((relationship: string) => { + if (hasValue(normalized[relationship])) { + const resourceType = getResourceType(normalized, relationship); + const resourceConstructor = NormalizedDSOFactory.getConstructor(resourceType); + if (Array.isArray(normalized[relationship])) { + // without the setTimeout, the actions inside requestService.configure + // are dispatched, but sometimes don't arrive. I'm unsure why atm. + setTimeout(() => { + normalized[relationship].forEach((href: string) => { + this.requestService.configure(href, resourceConstructor) + }); + }, 0); + + links[relationship] = normalized[relationship].map((href: string) => { + return this.buildSingle(href, resourceConstructor); + }); + } + else { + // without the setTimeout, the actions inside requestService.configure + // are dispatched, but sometimes don't arrive. I'm unsure why atm. + setTimeout(() => { + this.requestService.configure(normalized[relationship], resourceConstructor); + },0); + + links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor); + } + } + }); + + const domainModel = getMapsTo(normalized.constructor); + return Object.assign(new domainModel(), normalized, links); + } +} diff --git a/src/app/core/cache/cache.reducers.ts b/src/app/core/cache/cache.reducers.ts index 2edd1e8ebf87f1e00cd56840c23d30d1794cd50c..b5cd5c7b41f82287a8c232751b9dd48c6d897d40 100644 --- a/src/app/core/cache/cache.reducers.ts +++ b/src/app/core/cache/cache.reducers.ts @@ -1,14 +1,14 @@ import { combineReducers } from "@ngrx/store"; -import { RequestCacheState, requestCacheReducer } from "./request-cache.reducer"; +import { ResponseCacheState, responseCacheReducer } from "./response-cache.reducer"; import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer"; export interface CacheState { - request: RequestCacheState, + response: ResponseCacheState, object: ObjectCacheState } export const reducers = { - request: requestCacheReducer, + response: responseCacheReducer, object: objectCacheReducer }; diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..57b4f6334664ee24c459a07a11437f02686d47c0 --- /dev/null +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -0,0 +1,43 @@ +import { inheritSerialization, autoserialize } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { Bitstream } from "../../shared/bitstream.model"; +import { mapsTo } from "../builders/build-decorators"; + +@mapsTo(Bitstream) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedBitstream extends NormalizedDSpaceObject { + + /** + * The size of this bitstream in bytes(?) + */ + @autoserialize + size: number; + + /** + * The relative path to this Bitstream's file + */ + url: string; + + /** + * The mime type of this Bitstream + */ + mimetype: string; + + /** + * The description of this Bitstream + */ + description: string; + + /** + * An array of Bundles that are direct parents of this Bitstream + */ + parents: Array<string>; + + /** + * The Bundle that owns this Bitstream + */ + owner: string; + + @autoserialize + retrieve: string; +} diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb0e7b07082d721887bccb22ade6b33d9ac77dc7 --- /dev/null +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -0,0 +1,30 @@ +import { autoserialize, inheritSerialization } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { Bundle } from "../../shared/bundle.model"; +import { mapsTo, relationship } from "../builders/build-decorators"; +import { NormalizedDSOType } from "./normalized-dspace-object-type"; + +@mapsTo(Bundle) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedBundle extends NormalizedDSpaceObject { + /** + * The primary bitstream of this Bundle + */ + @autoserialize + @relationship(NormalizedDSOType.NormalizedBitstream) + primaryBitstream: string; + + /** + * An array of Items that are direct parents of this Bundle + */ + parents: Array<string>; + + /** + * The Item that owns this Bundle + */ + owner: string; + + @autoserialize + @relationship(NormalizedDSOType.NormalizedBitstream) + bitstreams: Array<string>; +} diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b31f34837e0e57780490c831495d4907e8312be --- /dev/null +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { Collection } from "../../shared/collection.model"; +import { mapsTo, relationship } from "../builders/build-decorators"; +import { NormalizedDSOType } from "./normalized-dspace-object-type"; + +@mapsTo(Collection) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedCollection extends NormalizedDSpaceObject { + + /** + * A string representing the unique handle of this Collection + */ + @autoserialize + handle: string; + + /** + * The Bitstream that represents the logo of this Collection + */ + logo: string; + + /** + * An array of Collections that are direct parents of this Collection + */ + parents: Array<string>; + + /** + * The Collection that owns this Collection + */ + owner: string; + + @autoserialize + @relationship(NormalizedDSOType.NormalizedItem) + items: Array<string>; + +} diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..774abcc979baa819a08cf3352b87f18f2908990e --- /dev/null +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { Community } from "../../shared/community.model"; +import { mapsTo, relationship } from "../builders/build-decorators"; +import { NormalizedDSOType } from "./normalized-dspace-object-type"; + +@mapsTo(Community) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedCommunity extends NormalizedDSpaceObject { + + /** + * A string representing the unique handle of this Community + */ + @autoserialize + handle: string; + + /** + * The Bitstream that represents the logo of this Community + */ + logo: string; + + /** + * An array of Communities that are direct parents of this Community + */ + parents: Array<string>; + + /** + * The Community that owns this Community + */ + owner: string; + + @autoserialize + @relationship(NormalizedDSOType.NormalizedCollection) + collections: Array<string>; + +} diff --git a/src/app/core/cache/models/normalized-dspace-object-factory.ts b/src/app/core/cache/models/normalized-dspace-object-factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..052f7be3ee9bbf1cb9ee2bc6092736ccb757adfd --- /dev/null +++ b/src/app/core/cache/models/normalized-dspace-object-factory.ts @@ -0,0 +1,33 @@ +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { NormalizedBitstream } from "./normalized-bitstream.model"; +import { NormalizedBundle } from "./normalized-bundle.model"; +import { NormalizedItem } from "./normalized-item.model"; +import { NormalizedCollection } from "./normalized-collection.model"; +import { GenericConstructor } from "../../shared/generic-constructor"; +import { NormalizedDSOType } from "./normalized-dspace-object-type"; +import { NormalizedCommunity } from "./normalized-community.model"; + +export class NormalizedDSOFactory { + public static getConstructor(type: NormalizedDSOType): GenericConstructor<NormalizedDSpaceObject> { + switch (type) { + case NormalizedDSOType.NormalizedBitstream: { + return NormalizedBitstream + } + case NormalizedDSOType.NormalizedBundle: { + return NormalizedBundle + } + case NormalizedDSOType.NormalizedItem: { + return NormalizedItem + } + case NormalizedDSOType.NormalizedCollection: { + return NormalizedCollection + } + case NormalizedDSOType.NormalizedCommunity: { + return NormalizedCommunity + } + default: { + return undefined; + } + } + } +} diff --git a/src/app/core/cache/models/normalized-dspace-object-type.ts b/src/app/core/cache/models/normalized-dspace-object-type.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ac9215b441094d3ec1fbe0010a886fb2ba3e102 --- /dev/null +++ b/src/app/core/cache/models/normalized-dspace-object-type.ts @@ -0,0 +1,7 @@ +export enum NormalizedDSOType { + NormalizedBitstream, + NormalizedBundle, + NormalizedItem, + NormalizedCollection, + NormalizedCommunity +} diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..29688d5f9dff4854cad7549c973665f212165dbd --- /dev/null +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -0,0 +1,52 @@ +import { autoserialize, autoserializeAs } from "cerialize"; +import { CacheableObject } from "../object-cache.reducer"; +import { Metadatum } from "../../shared/metadatum.model"; + +/** + * An abstract model class for a DSpaceObject. + */ +export abstract class NormalizedDSpaceObject implements CacheableObject { + + @autoserialize + self: string; + + /** + * The human-readable identifier of this DSpaceObject + */ + @autoserialize + id: string; + + /** + * The universally unique identifier of this DSpaceObject + */ + @autoserialize + uuid: string; + + /** + * A string representing the kind of DSpaceObject, e.g. community, item, … + */ + type: string; + + /** + * The name for this DSpaceObject + */ + @autoserialize + name: string; + + /** + * An array containing all metadata of this DSpaceObject + */ + @autoserializeAs(Metadatum) + metadata: Array<Metadatum>; + + /** + * An array of DSpaceObjects that are direct parents of this DSpaceObject + */ + @autoserialize + parents: Array<string>; + + /** + * The DSpaceObject that owns this DSpaceObject + */ + owner: string; +} diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..cdd3acdb923eccc5bdda574dded3c192b9b95ebb --- /dev/null +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -0,0 +1,47 @@ +import { inheritSerialization, autoserialize } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; +import { Item } from "../../shared/item.model"; +import { mapsTo, relationship } from "../builders/build-decorators"; +import { NormalizedDSOType } from "./normalized-dspace-object-type"; + +@mapsTo(Item) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedItem extends NormalizedDSpaceObject { + + /** + * A string representing the unique handle of this Item + */ + @autoserialize + handle: string; + + /** + * The Date of the last modification of this Item + */ + lastModified: Date; + + /** + * A boolean representing if this Item is currently archived or not + */ + isArchived: boolean; + + /** + * A boolean representing if this Item is currently withdrawn or not + */ + isWithdrawn: boolean; + + /** + * An array of Collections that are direct parents of this Item + */ + @autoserialize + @relationship(NormalizedDSOType.NormalizedCollection) + parents: Array<string>; + + /** + * The Collection that owns this Item + */ + owner: string; + + @autoserialize + @relationship(NormalizedDSOType.NormalizedBundle) + bundles: Array<string>; +} diff --git a/src/app/core/shared/pagination-options.model.ts b/src/app/core/cache/models/pagination-options.model.ts similarity index 100% rename from src/app/core/shared/pagination-options.model.ts rename to src/app/core/cache/models/pagination-options.model.ts diff --git a/src/app/core/cache/models/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5adc78062a747984104149e6313e2eb41885ab7b --- /dev/null +++ b/src/app/core/cache/models/self-link.model.ts @@ -0,0 +1,10 @@ +import { autoserialize } from "cerialize"; + +export class SelfLink { + + @autoserialize + self: string; + + @autoserialize + uuid: string; +} diff --git a/src/app/core/shared/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts similarity index 100% rename from src/app/core/shared/sort-options.model.ts rename to src/app/core/cache/models/sort-options.model.ts diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 23b01882161aedcec48eb6055d7b4ba0b38ed817..85e1fdc2b303586befcf5fb7ad8cc2e1dc074e8d 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -12,6 +12,7 @@ import { CacheEntry } from "./cache-entry"; */ export interface CacheableObject { uuid: string; + self?: string; } /** diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9093093f50da4717ef78100fc896b2d82091a92a..ec0bea4a97b8afe3fa14aef059b2bb0fff12fcd6 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -60,6 +60,11 @@ export class ObjectCacheService { .map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data)); } + getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> { + return this.store.select<string>('core', 'index', 'href', href) + .flatMap((uuid: string) => this.get(uuid, type)) + } + /** * Get an observable for an array of objects of the same type * with the specified UUIDs @@ -104,6 +109,25 @@ export class ObjectCacheService { return result; } + /** + * Check whether the object with the specified self link is cached + * + * @param href + * The self link of the object to check + * @return boolean + * true if the object with the specified self link is cached, + * false otherwise + */ + hasBySelfLink(href: string): boolean { + let result: boolean = false; + + this.store.select<string>('core', 'index', 'href', href) + .take(1) + .subscribe((uuid: string) => result = this.has(uuid)); + + return result; + } + /** * Check whether an ObjectCacheEntry should still be cached * diff --git a/src/app/core/cache/request-cache.actions.ts b/src/app/core/cache/request-cache.actions.ts deleted file mode 100644 index 78c6692d71a2e19d6315ded4b82213f16bd2a82e..0000000000000000000000000000000000000000 --- a/src/app/core/cache/request-cache.actions.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { OpaqueToken } from "@angular/core"; -import { Action } from "@ngrx/store"; -import { type } from "../../shared/ngrx/type"; -import { PaginationOptions } from "../shared/pagination-options.model"; -import { SortOptions } from "../shared/sort-options.model"; - -/** - * The list of RequestCacheAction type definitions - */ -export const RequestCacheActionTypes = { - FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'), - FIND_ALL: type('dspace/core/cache/request/FIND_ALL'), - SUCCESS: type('dspace/core/cache/request/SUCCESS'), - ERROR: type('dspace/core/cache/request/ERROR'), - REMOVE: type('dspace/core/cache/request/REMOVE'), - RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS') -}; - -/** - * An ngrx action to find all objects of a certain type - */ -export class RequestCacheFindAllAction implements Action { - type = RequestCacheActionTypes.FIND_ALL; - payload: { - key: string, - service: OpaqueToken, - scopeID: string, - paginationOptions: PaginationOptions, - sortOptions: SortOptions - }; - - /** - * Create a new RequestCacheFindAllAction - * - * @param key - * the key under which to cache this request, should be unique - * @param service - * the name of the service that initiated the action - * @param scopeID - * the id of an optional scope object - * @param paginationOptions - * the pagination options - * @param sortOptions - * the sort options - */ - constructor( - key: string, - service: OpaqueToken, - scopeID?: string, - paginationOptions: PaginationOptions = new PaginationOptions(), - sortOptions: SortOptions = new SortOptions() - ) { - this.payload = { - key, - service, - scopeID, - paginationOptions, - sortOptions - } - } -} - -/** - * An ngrx action to find objects by id - */ -export class RequestCacheFindByIDAction implements Action { - type = RequestCacheActionTypes.FIND_BY_ID; - payload: { - key: string, - service: OpaqueToken, - resourceID: string - }; - - /** - * Create a new RequestCacheFindByIDAction - * - * @param key - * the key under which to cache this request, should be unique - * @param service - * the name of the service that initiated the action - * @param resourceID - * the ID of the resource to find - */ - constructor( - key: string, - service: OpaqueToken, - resourceID: string - ) { - this.payload = { - key, - service, - resourceID - } - } -} - -/** - * An ngrx action to indicate a request was returned successful - */ -export class RequestCacheSuccessAction implements Action { - type = RequestCacheActionTypes.SUCCESS; - payload: { - key: string, - resourceUUIDs: Array<string>, - timeAdded: number, - msToLive: number - }; - - /** - * Create a new RequestCacheSuccessAction - * - * @param key - * the key under which cache this request is cached, - * should be identical to the one used in the corresponding - * find action - * @param resourceUUIDs - * the UUIDs returned from the backend - * @param timeAdded - * the time it was returned - * @param msToLive - * the amount of milliseconds before it should expire - */ - constructor(key: string, resourceUUIDs: Array<string>, timeAdded, msToLive: number) { - this.payload = { - key, - resourceUUIDs, - timeAdded, - msToLive - }; - } -} - -/** - * An ngrx action to indicate a request failed - */ -export class RequestCacheErrorAction implements Action { - type = RequestCacheActionTypes.ERROR; - payload: { - key: string, - errorMessage: string - }; - - /** - * Create a new RequestCacheErrorAction - * - * @param key - * the key under which cache this request is cached, - * should be identical to the one used in the corresponding - * find action - * @param errorMessage - * A message describing the reason the request failed - */ - constructor(key: string, errorMessage: string) { - this.payload = { - key, - errorMessage - }; - } -} - -/** - * An ngrx action to remove a request from the cache - */ -export class RequestCacheRemoveAction implements Action { - type = RequestCacheActionTypes.REMOVE; - payload: string; - - /** - * Create a new RequestCacheRemoveAction - * @param key - * The key of the request to remove - */ - constructor(key: string) { - this.payload = key; - } -} - -/** - * An ngrx action to reset the timeAdded property of all cached objects - */ -export class ResetRequestCacheTimestampsAction implements Action { - type = RequestCacheActionTypes.RESET_TIMESTAMPS; - payload: number; - - /** - * Create a new ResetObjectCacheTimestampsAction - * - * @param newTimestamp - * the new timeAdded all objects should get - */ - constructor(newTimestamp: number) { - this.payload = newTimestamp; - } -} - -/** - * A type to encompass all RequestCacheActions - */ -export type RequestCacheAction - = RequestCacheFindAllAction - | RequestCacheFindByIDAction - | RequestCacheSuccessAction - | RequestCacheErrorAction - | RequestCacheRemoveAction - | ResetRequestCacheTimestampsAction; diff --git a/src/app/core/cache/request-cache.reducer.spec.ts b/src/app/core/cache/request-cache.reducer.spec.ts deleted file mode 100644 index a478f0c2f5c4914673108b30ad25178e0a344e33..0000000000000000000000000000000000000000 --- a/src/app/core/cache/request-cache.reducer.spec.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { requestCacheReducer, RequestCacheState } from "./request-cache.reducer"; -import { - RequestCacheRemoveAction, RequestCacheFindByIDAction, - RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction, - ResetRequestCacheTimestampsAction -} from "./request-cache.actions"; -import deepFreeze = require("deep-freeze"); -import { OpaqueToken } from "@angular/core"; -import { PaginationOptions } from "../shared/pagination-options.model"; - -class NullAction extends RequestCacheRemoveAction { - type = null; - payload = null; - - constructor() { - super(null); - } -} - -describe("requestCacheReducer", () => { - const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; - const services = [new OpaqueToken('service1'), new OpaqueToken('service2')]; - const msToLive = 900000; - const uuids = [ - "9e32a2e2-6b91-4236-a361-995ccdc14c60", - "598ce822-c357-46f3-ab70-63724d02d6ad", - "be8325f7-243b-49f4-8a4b-df2b793ff3b5" - ]; - const resourceID = "9978"; - const paginationOptions: PaginationOptions = { - "id": "test", - "currentPage": 1, - "pageSizeOptions": [5, 10, 20, 40, 60, 80, 100], - "disabled": false, - "boundaryLinks": false, - "directionLinks": true, - "ellipses": true, - "maxSize": 0, - "pageSize": 10, - "rotate": false, - "size": 'sm' - }; - const sortOptions = { "field": "id", "direction": 0 }; - const testState = { - [keys[0]]: { - "key": keys[0], - "service": services[0], - "resourceUUIDs": [uuids[0], uuids[1]], - "isLoading": false, - "paginationOptions": paginationOptions, - "sortOptions": sortOptions, - "timeAdded": new Date().getTime(), - "msToLive": msToLive - }, - [keys[1]]: { - "key": keys[1], - "service": services[1], - "resourceID": resourceID, - "resourceUUIDs": [uuids[2]], - "isLoading": false, - "timeAdded": new Date().getTime(), - "msToLive": msToLive - } - }; - deepFreeze(testState); - const errorState: {} = { - [keys[0]]: { - errorMessage: 'error', - resourceUUIDs: uuids - } - }; - deepFreeze(errorState); - - - it("should return the current state when no valid actions have been made", () => { - const action = new NullAction(); - const newState = requestCacheReducer(testState, action); - - expect(newState).toEqual(testState); - }); - - it("should start with an empty cache", () => { - const action = new NullAction(); - const initialState = requestCacheReducer(undefined, action); - - expect(initialState).toEqual(Object.create(null)); - }); - - describe("FIND_BY_ID", () => { - const action = new RequestCacheFindByIDAction(keys[0], services[0], resourceID); - - it("should perform the action without affecting the previous state", () => { - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should add the request to the cache", () => { - const state = Object.create(null); - const newState = requestCacheReducer(state, action); - expect(newState[keys[0]].key).toBe(keys[0]); - expect(newState[keys[0]].service).toEqual(services[0]); - expect(newState[keys[0]].resourceID).toBe(resourceID); - }); - - it("should set isLoading to true", () => { - const state = Object.create(null); - const newState = requestCacheReducer(state, action); - expect(newState[keys[0]].isLoading).toBe(true); - }); - - it("should remove any previous error message or resourceUUID for the request", () => { - const newState = requestCacheReducer(errorState, action); - expect(newState[keys[0]].resourceUUIDs.length).toBe(0); - expect(newState[keys[0]].errorMessage).toBeUndefined(); - }); - }); - - describe("FIND_ALL", () => { - const action = new RequestCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions); - - it("should perform the action without affecting the previous state", () => { - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should add the request to the cache", () => { - const state = Object.create(null); - const newState = requestCacheReducer(state, action); - expect(newState[keys[0]].key).toBe(keys[0]); - expect(newState[keys[0]].service).toEqual(services[0]); - expect(newState[keys[0]].scopeID).toBe(resourceID); - expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions); - expect(newState[keys[0]].sortOptions).toEqual(sortOptions); - }); - - it("should set isLoading to true", () => { - const state = Object.create(null); - const newState = requestCacheReducer(state, action); - expect(newState[keys[0]].isLoading).toBe(true); - }); - - it("should remove any previous error message or resourceUUIDs for the request", () => { - const newState = requestCacheReducer(errorState, action); - expect(newState[keys[0]].resourceUUIDs.length).toBe(0); - expect(newState[keys[0]].errorMessage).toBeUndefined(); - }); - }); - - describe("SUCCESS", () => { - const successUUIDs = [uuids[0], uuids[2]]; - const successTimeAdded = new Date().getTime(); - const successMsToLive = 5; - const action = new RequestCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive); - - it("should perform the action without affecting the previous state", () => { - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should add the response to the cached request", () => { - const newState = requestCacheReducer(testState, action); - expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs); - expect(newState[keys[0]].timeAdded).toBe(successTimeAdded); - expect(newState[keys[0]].msToLive).toBe(successMsToLive); - }); - - it("should set isLoading to false", () => { - const newState = requestCacheReducer(testState, action); - expect(newState[keys[0]].isLoading).toBe(false); - }); - - it("should remove any previous error message for the request", () => { - const newState = requestCacheReducer(errorState, action); - expect(newState[keys[0]].errorMessage).toBeUndefined(); - }); - }); - - describe("ERROR", () => { - const errorMsg = 'errorMsg'; - const action = new RequestCacheErrorAction(keys[0], errorMsg); - - it("should perform the action without affecting the previous state", () => { - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should set an error message for the request", () => { - const newState = requestCacheReducer(errorState, action); - expect(newState[keys[0]].errorMessage).toBe(errorMsg); - }); - - it("should set isLoading to false", () => { - const newState = requestCacheReducer(testState, action); - expect(newState[keys[0]].isLoading).toBe(false); - }); - }); - - describe("REMOVE", () => { - it("should perform the action without affecting the previous state", () => { - const action = new RequestCacheRemoveAction(keys[0]); - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should remove the specified request from the cache", () => { - const action = new RequestCacheRemoveAction(keys[0]); - const newState = requestCacheReducer(testState, action); - expect(testState[keys[0]]).not.toBeUndefined(); - expect(newState[keys[0]]).toBeUndefined(); - }); - - it("shouldn't do anything when the specified key isn't cached", () => { - const wrongKey = "this isn't cached"; - const action = new RequestCacheRemoveAction(wrongKey); - const newState = requestCacheReducer(testState, action); - expect(testState[wrongKey]).toBeUndefined(); - expect(newState).toEqual(testState); - }); - }); - - describe("RESET_TIMESTAMPS", () => { - const newTimeStamp = new Date().getTime(); - const action = new ResetRequestCacheTimestampsAction(newTimeStamp); - - it("should perform the action without affecting the previous state", () => { - //testState has already been frozen above - requestCacheReducer(testState, action); - }); - - it("should set the timestamp of all requests in the cache", () => { - const newState = requestCacheReducer(testState, action); - Object.keys(newState).forEach((key) => { - expect(newState[key].timeAdded).toEqual(newTimeStamp); - }); - }); - - }); - - -}); diff --git a/src/app/core/cache/request-cache.reducer.ts b/src/app/core/cache/request-cache.reducer.ts deleted file mode 100644 index 0aa2e8c920df13eed975a3bd9a47bddc30305393..0000000000000000000000000000000000000000 --- a/src/app/core/cache/request-cache.reducer.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { PaginationOptions } from "../shared/pagination-options.model"; -import { SortOptions } from "../shared/sort-options.model"; -import { - RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction, - RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction, - RequestCacheRemoveAction, ResetRequestCacheTimestampsAction -} from "./request-cache.actions"; -import { OpaqueToken } from "@angular/core"; -import { CacheEntry } from "./cache-entry"; -import { hasValue } from "../../shared/empty.util"; - -/** - * An entry in the RequestCache - */ -export class RequestCacheEntry implements CacheEntry { - service: OpaqueToken; - key: string; - scopeID: string; - resourceID: string; - resourceUUIDs: Array<String>; - resourceType: String; - isLoading: boolean; - errorMessage: string; - paginationOptions: PaginationOptions; - sortOptions: SortOptions; - timeAdded: number; - msToLive: number; -} - -/** - * The RequestCache State - */ -export interface RequestCacheState { - [key: string]: RequestCacheEntry -} - -// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) -const initialState = Object.create(null); - -/** - * The RequestCache Reducer - * - * @param state - * the current state - * @param action - * the action to perform on the state - * @return RequestCacheState - * the new state - */ -export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => { - switch (action.type) { - - case RequestCacheActionTypes.FIND_ALL: { - return findAllRequest(state, <RequestCacheFindAllAction> action); - } - - case RequestCacheActionTypes.FIND_BY_ID: { - return findByIDRequest(state, <RequestCacheFindByIDAction> action); - } - - case RequestCacheActionTypes.SUCCESS: { - return success(state, <RequestCacheSuccessAction> action); - } - - case RequestCacheActionTypes.ERROR: { - return error(state, <RequestCacheErrorAction> action); - } - - case RequestCacheActionTypes.REMOVE: { - return removeFromCache(state, <RequestCacheRemoveAction> action); - } - - case RequestCacheActionTypes.RESET_TIMESTAMPS: { - return resetRequestCacheTimestamps(state, <ResetRequestCacheTimestampsAction>action) - } - - default: { - return state; - } - } -}; - -/** - * Add a FindAll request to the cache - * - * @param state - * the current state - * @param action - * a RequestCacheFindAllAction - * @return RequestCacheState - * the new state, with the request added, or overwritten - */ -function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState { - return Object.assign({}, state, { - [action.payload.key]: { - key: action.payload.key, - service: action.payload.service, - scopeID: action.payload.scopeID, - resourceUUIDs: [], - isLoading: true, - errorMessage: undefined, - paginationOptions: action.payload.paginationOptions, - sortOptions: action.payload.sortOptions - } - }); -} - -/** - * Add a FindByID request to the cache - * - * @param state - * the current state - * @param action - * a RequestCacheFindByIDAction - * @return RequestCacheState - * the new state, with the request added, or overwritten - */ -function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState { - return Object.assign({}, state, { - [action.payload.key]: { - key: action.payload.key, - service: action.payload.service, - resourceID: action.payload.resourceID, - resourceUUIDs: [], - isLoading: true, - errorMessage: undefined, - } - }); -} - -/** - * Update a cached request with a successful response - * - * @param state - * the current state - * @param action - * a RequestCacheSuccessAction - * @return RequestCacheState - * the new state, with the response added to the request - */ -function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState { - return Object.assign({}, state, { - [action.payload.key]: Object.assign({}, state[action.payload.key], { - isLoading: false, - resourceUUIDs: action.payload.resourceUUIDs, - errorMessage: undefined, - timeAdded: action.payload.timeAdded, - msToLive: action.payload.msToLive - }) - }); -} - -/** - * Update a cached request with an error - * - * @param state - * the current state - * @param action - * a RequestCacheSuccessAction - * @return RequestCacheState - * the new state, with the error added to the request - */ -function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState { - return Object.assign({}, state, { - [action.payload.key]: Object.assign({}, state[action.payload.key], { - isLoading: false, - errorMessage: action.payload.errorMessage - }) - }); -} - -/** - * Remove a request from the cache - * - * @param state - * the current state - * @param action - * an RequestCacheRemoveAction - * @return RequestCacheState - * the new state, with the request removed if it existed. - */ -function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState { - if (hasValue(state[action.payload])) { - let newCache = Object.assign({}, state); - delete newCache[action.payload]; - - return newCache; - } - else { - return state; - } -} - -/** - * Set the timeAdded timestamp of every cached request to the specified value - * - * @param state - * the current state - * @param action - * a ResetRequestCacheTimestampsAction - * @return RequestCacheState - * the new state, with all timeAdded timestamps set to the specified value - */ -function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState { - let newState = Object.create(null); - Object.keys(state).forEach(key => { - newState[key] = Object.assign({}, state[key], { - timeAdded: action.payload - }); - }); - return newState; -} diff --git a/src/app/core/cache/request-cache.service.spec.ts b/src/app/core/cache/request-cache.service.spec.ts deleted file mode 100644 index c29addd23f2eef52d790a5448e897ca6b5853e25..0000000000000000000000000000000000000000 --- a/src/app/core/cache/request-cache.service.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { RequestCacheService } from "./request-cache.service"; -import { Store } from "@ngrx/store"; -import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer"; -import { OpaqueToken } from "@angular/core"; -import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions"; -import { Observable } from "rxjs"; -import { PaginationOptions } from "../shared/pagination-options.model"; - -describe("RequestCacheService", () => { - let service: RequestCacheService; - let store: Store<RequestCacheState>; - - const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; - const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')]; - const resourceID = "9978"; - const paginationOptions: PaginationOptions = { - "id": "test", - "currentPage": 1, - "pageSizeOptions": [5, 10, 20, 40, 60, 80, 100], - "disabled": false, - "boundaryLinks": false, - "directionLinks": true, - "ellipses": true, - "maxSize": 0, - "pageSize": 10, - "rotate": false, - "size": 'sm' - }; - const sortOptions = { "field": "id", "direction": 0 }; - const timestamp = new Date().getTime(); - const validCacheEntry = (key) => { - return { - key: key, - timeAdded: timestamp, - msToLive: 24 * 60 * 60 * 1000 // a day - } - }; - const invalidCacheEntry = (key) => { - return { - key: key, - timeAdded: 0, - msToLive: 0 - } - }; - - beforeEach(() => { - store = new Store<RequestCacheState>(undefined, undefined, undefined); - spyOn(store, 'dispatch'); - service = new RequestCacheService(store); - spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp }); - }); - - describe("findAll", () => { - beforeEach(() => { - spyOn(service, "get").and.callFake((key) => Observable.of({key: key})); - }); - describe("if the key isn't cached", () => { - beforeEach(() => { - spyOn(service, "has").and.returnValue(false); - }); - it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => { - service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions); - expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions)) - }); - it("should return an observable of the newly cached request with the specified key", () => { - let result: RequestCacheEntry; - service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry); - expect(result.key).toEqual(keys[0]); - }); - }); - describe("if the key is already cached", () => { - beforeEach(() => { - spyOn(service, "has").and.returnValue(true); - }); - it("shouldn't dispatch anything", () => { - service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - it("should return an observable of the existing cached request with the specified key", () => { - let result: RequestCacheEntry; - service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry); - expect(result.key).toEqual(keys[0]); - }); - }); - }); - - describe("findById", () => { - beforeEach(() => { - spyOn(service, "get").and.callFake((key) => Observable.of({key: key})); - }); - describe("if the key isn't cached", () => { - beforeEach(() => { - spyOn(service, "has").and.returnValue(false); - }); - it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => { - service.findById(keys[0], serviceTokens[0], resourceID); - expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindByIDAction(keys[0], serviceTokens[0], resourceID)) - }); - it("should return an observable of the newly cached request with the specified key", () => { - let result: RequestCacheEntry; - service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry); - expect(result.key).toEqual(keys[0]); - }); - }); - describe("if the key is already cached", () => { - beforeEach(() => { - spyOn(service, "has").and.returnValue(true); - }); - it("shouldn't dispatch anything", () => { - service.findById(keys[0], serviceTokens[0], resourceID); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - it("should return an observable of the existing cached request with the specified key", () => { - let result: RequestCacheEntry; - service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry); - expect(result.key).toEqual(keys[0]); - }); - }); - }); - - describe("get", () => { - it("should return an observable of the cached request with the specified key", () => { - spyOn(store, "select").and.callFake((...args:Array<any>) => { - return Observable.of(validCacheEntry(args[args.length - 1])); - }); - - let testObj: RequestCacheEntry; - service.get(keys[1]).take(1).subscribe(entry => testObj = entry); - expect(testObj.key).toEqual(keys[1]); - }); - - it("should not return a cached request that has exceeded its time to live", () => { - spyOn(store, "select").and.callFake((...args:Array<any>) => { - return Observable.of(invalidCacheEntry(args[args.length - 1])); - }); - - let getObsHasFired = false; - const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true); - expect(getObsHasFired).toBe(false); - subscription.unsubscribe(); - }); - }); - - describe("has", () => { - it("should return true if the request with the supplied key is cached and still valid", () => { - spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1]))); - expect(service.has(keys[1])).toBe(true); - }); - - it("should return false if the request with the supplied key isn't cached", () => { - spyOn(store, 'select').and.returnValue(Observable.of(undefined)); - expect(service.has(keys[1])).toBe(false); - }); - - it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => { - spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1]))); - expect(service.has(keys[1])).toBe(false); - }); - }); -}); diff --git a/src/app/core/cache/request-cache.service.ts b/src/app/core/cache/request-cache.service.ts deleted file mode 100644 index efa7b0d426fc564b7fa34dac2a3b37c875efd4e1..0000000000000000000000000000000000000000 --- a/src/app/core/cache/request-cache.service.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Injectable, OpaqueToken } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer"; -import { Observable } from "rxjs"; -import { hasNoValue } from "../../shared/empty.util"; -import { - RequestCacheRemoveAction, RequestCacheFindAllAction, - RequestCacheFindByIDAction -} from "./request-cache.actions"; -import { SortOptions } from "../shared/sort-options.model"; -import { PaginationOptions } from "../shared/pagination-options.model"; - -/** - * A service to interact with the request cache - */ -@Injectable() -export class RequestCacheService { - constructor( - private store: Store<RequestCacheState> - ) {} - - /** - * Start a new findAll request - * - * This will send a new findAll request to the backend, - * and store the request parameters and the fact that - * the request is pending - * - * @param key - * the key should be a unique identifier for the request and its parameters - * @param service - * the service that initiated the request - * @param scopeID - * the id of an optional scope object - * @param paginationOptions - * the pagination options (optional) - * @param sortOptions - * the sort options (optional) - * @return Observable<RequestCacheEntry> - * an observable of the RequestCacheEntry for this request - */ - findAll( - key: string, - service: OpaqueToken, - scopeID?: string, - paginationOptions?: PaginationOptions, - sortOptions?: SortOptions - ): Observable<RequestCacheEntry> { - if (!this.has(key)) { - this.store.dispatch(new RequestCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions)); - } - return this.get(key); - } - - /** - * Start a new findById request - * - * This will send a new findById request to the backend, - * and store the request parameters and the fact that - * the request is pending - * - * @param key - * the key should be a unique identifier for the request and its parameters - * @param service - * the service that initiated the request - * @param resourceID - * the ID of the resource to find - * @return Observable<RequestCacheEntry> - * an observable of the RequestCacheEntry for this request - */ - findById( - key: string, - service: OpaqueToken, - resourceID: string - ): Observable<RequestCacheEntry> { - if (!this.has(key)) { - this.store.dispatch(new RequestCacheFindByIDAction(key, service, resourceID)); - } - return this.get(key); - } - - /** - * Get an observable of the request with the specified key - * - * @param key - * the key of the request to get - * @return Observable<RequestCacheEntry> - * an observable of the RequestCacheEntry with the specified key - */ - get(key: string): Observable<RequestCacheEntry> { - return this.store.select<RequestCacheEntry>('core', 'cache', 'request', key) - .filter(entry => this.isValid(entry)) - .distinctUntilChanged() - } - - /** - * Check whether the request with the specified key is cached - * - * @param key - * the key of the request to check - * @return boolean - * true if the request with the specified key is cached, - * false otherwise - */ - has(key: string): boolean { - let result: boolean; - - this.store.select<RequestCacheEntry>('core', 'cache', 'request', key) - .take(1) - .subscribe(entry => result = this.isValid(entry)); - - return result; - } - - /** - * Check whether a RequestCacheEntry should still be cached - * - * @param entry - * the entry to check - * @return boolean - * false if the entry is null, undefined, or its time to - * live has been exceeded, true otherwise - */ - private isValid(entry: RequestCacheEntry): boolean { - if (hasNoValue(entry)) { - return false; - } - else { - const timeOutdated = entry.timeAdded + entry.msToLive; - const isOutDated = new Date().getTime() > timeOutdated; - if (isOutDated) { - this.store.dispatch(new RequestCacheRemoveAction(entry.key)); - } - return !isOutDated; - } - } - -} diff --git a/src/app/core/cache/response-cache.actions.ts b/src/app/core/cache/response-cache.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..45f78f10b7342b721acf7e6812adb1ed9af6aa36 --- /dev/null +++ b/src/app/core/cache/response-cache.actions.ts @@ -0,0 +1,69 @@ +import { Action } from "@ngrx/store"; +import { type } from "../../shared/ngrx/type"; +import { Response } from "./response-cache.models"; + +/** + * The list of ResponseCacheAction type definitions + */ +export const ResponseCacheActionTypes = { + ADD: type('dspace/core/cache/response/ADD'), + REMOVE: type('dspace/core/cache/response/REMOVE'), + RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS') +}; + +export class ResponseCacheAddAction implements Action { + type = ResponseCacheActionTypes.ADD; + payload: { + key: string, + response: Response + timeAdded: number; + msToLive: number; + }; + + constructor(key: string, response: Response, timeAdded: number, msToLive: number) { + this.payload = { key, response, timeAdded, msToLive }; + } +} + +/** + * An ngrx action to remove a request from the cache + */ +export class ResponseCacheRemoveAction implements Action { + type = ResponseCacheActionTypes.REMOVE; + payload: string; + + /** + * Create a new ResponseCacheRemoveAction + * @param key + * The key of the request to remove + */ + constructor(key: string) { + this.payload = key; + } +} + +/** + * An ngrx action to reset the timeAdded property of all cached objects + */ +export class ResetResponseCacheTimestampsAction implements Action { + type = ResponseCacheActionTypes.RESET_TIMESTAMPS; + payload: number; + + /** + * Create a new ResetObjectCacheTimestampsAction + * + * @param newTimestamp + * the new timeAdded all objects should get + */ + constructor(newTimestamp: number) { + this.payload = newTimestamp; + } +} + +/** + * A type to encompass all ResponseCacheActions + */ +export type ResponseCacheAction + = ResponseCacheAddAction + | ResponseCacheRemoveAction + | ResetResponseCacheTimestampsAction; diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts new file mode 100644 index 0000000000000000000000000000000000000000..741acf99a6ba97c7acd7945859ea02cffe49d163 --- /dev/null +++ b/src/app/core/cache/response-cache.models.ts @@ -0,0 +1,16 @@ +export class Response { + constructor(public isSuccessful: boolean) {} +} + +export class SuccessResponse extends Response { + constructor(public resourceUUIDs: Array<String>) { + super(true); + } +} + +export class ErrorResponse extends Response { + constructor(public errorMessage: string) { + super(false); + } +} + diff --git a/src/app/core/cache/response-cache.reducer.spec.ts b/src/app/core/cache/response-cache.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b084842f7d1295f66663a73770d1a412c7223fc4 --- /dev/null +++ b/src/app/core/cache/response-cache.reducer.spec.ts @@ -0,0 +1,225 @@ +import { responseCacheReducer, ResponseCacheState } from "./response-cache.reducer"; +import { + ResponseCacheRemoveAction, + ResetResponseCacheTimestampsAction +} from "./response-cache.actions"; +import deepFreeze = require("deep-freeze"); + +class NullAction extends ResponseCacheRemoveAction { + type = null; + payload = null; + + constructor() { + super(null); + } +} + +// describe("responseCacheReducer", () => { +// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; +// const services = [new OpaqueToken('service1'), new OpaqueToken('service2')]; +// const msToLive = 900000; +// const uuids = [ +// "9e32a2e2-6b91-4236-a361-995ccdc14c60", +// "598ce822-c357-46f3-ab70-63724d02d6ad", +// "be8325f7-243b-49f4-8a4b-df2b793ff3b5" +// ]; +// const resourceID = "9978"; +// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 }; +// const sortOptions = { "field": "id", "direction": 0 }; +// const testState = { +// [keys[0]]: { +// "key": keys[0], +// "service": services[0], +// "resourceUUIDs": [uuids[0], uuids[1]], +// "isLoading": false, +// "paginationOptions": paginationOptions, +// "sortOptions": sortOptions, +// "timeAdded": new Date().getTime(), +// "msToLive": msToLive +// }, +// [keys[1]]: { +// "key": keys[1], +// "service": services[1], +// "resourceID": resourceID, +// "resourceUUIDs": [uuids[2]], +// "isLoading": false, +// "timeAdded": new Date().getTime(), +// "msToLive": msToLive +// } +// }; +// deepFreeze(testState); +// const errorState: {} = { +// [keys[0]]: { +// errorMessage: 'error', +// resourceUUIDs: uuids +// } +// }; +// deepFreeze(errorState); +// +// +// it("should return the current state when no valid actions have been made", () => { +// const action = new NullAction(); +// const newState = responseCacheReducer(testState, action); +// +// expect(newState).toEqual(testState); +// }); +// +// it("should start with an empty cache", () => { +// const action = new NullAction(); +// const initialState = responseCacheReducer(undefined, action); +// +// expect(initialState).toEqual(Object.create(null)); +// }); +// +// describe("FIND_BY_ID", () => { +// const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID); +// +// it("should perform the action without affecting the previous state", () => { +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should add the request to the cache", () => { +// const state = Object.create(null); +// const newState = responseCacheReducer(state, action); +// expect(newState[keys[0]].key).toBe(keys[0]); +// expect(newState[keys[0]].service).toEqual(services[0]); +// expect(newState[keys[0]].resourceID).toBe(resourceID); +// }); +// +// it("should set responsePending to true", () => { +// const state = Object.create(null); +// const newState = responseCacheReducer(state, action); +// expect(newState[keys[0]].responsePending).toBe(true); +// }); +// +// it("should remove any previous error message or resourceUUID for the request", () => { +// const newState = responseCacheReducer(errorState, action); +// expect(newState[keys[0]].resourceUUIDs.length).toBe(0); +// expect(newState[keys[0]].errorMessage).toBeUndefined(); +// }); +// }); +// +// describe("FIND_ALL", () => { +// const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions); +// +// it("should perform the action without affecting the previous state", () => { +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should add the request to the cache", () => { +// const state = Object.create(null); +// const newState = responseCacheReducer(state, action); +// expect(newState[keys[0]].key).toBe(keys[0]); +// expect(newState[keys[0]].service).toEqual(services[0]); +// expect(newState[keys[0]].scopeID).toBe(resourceID); +// expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions); +// expect(newState[keys[0]].sortOptions).toEqual(sortOptions); +// }); +// +// it("should set responsePending to true", () => { +// const state = Object.create(null); +// const newState = responseCacheReducer(state, action); +// expect(newState[keys[0]].responsePending).toBe(true); +// }); +// +// it("should remove any previous error message or resourceUUIDs for the request", () => { +// const newState = responseCacheReducer(errorState, action); +// expect(newState[keys[0]].resourceUUIDs.length).toBe(0); +// expect(newState[keys[0]].errorMessage).toBeUndefined(); +// }); +// }); +// +// describe("SUCCESS", () => { +// const successUUIDs = [uuids[0], uuids[2]]; +// const successTimeAdded = new Date().getTime(); +// const successMsToLive = 5; +// const action = new ResponseCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive); +// +// it("should perform the action without affecting the previous state", () => { +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should add the response to the cached request", () => { +// const newState = responseCacheReducer(testState, action); +// expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs); +// expect(newState[keys[0]].timeAdded).toBe(successTimeAdded); +// expect(newState[keys[0]].msToLive).toBe(successMsToLive); +// }); +// +// it("should set responsePending to false", () => { +// const newState = responseCacheReducer(testState, action); +// expect(newState[keys[0]].responsePending).toBe(false); +// }); +// +// it("should remove any previous error message for the request", () => { +// const newState = responseCacheReducer(errorState, action); +// expect(newState[keys[0]].errorMessage).toBeUndefined(); +// }); +// }); +// +// describe("ERROR", () => { +// const errorMsg = 'errorMsg'; +// const action = new ResponseCacheErrorAction(keys[0], errorMsg); +// +// it("should perform the action without affecting the previous state", () => { +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should set an error message for the request", () => { +// const newState = responseCacheReducer(errorState, action); +// expect(newState[keys[0]].errorMessage).toBe(errorMsg); +// }); +// +// it("should set responsePending to false", () => { +// const newState = responseCacheReducer(testState, action); +// expect(newState[keys[0]].responsePending).toBe(false); +// }); +// }); +// +// describe("REMOVE", () => { +// it("should perform the action without affecting the previous state", () => { +// const action = new ResponseCacheRemoveAction(keys[0]); +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should remove the specified request from the cache", () => { +// const action = new ResponseCacheRemoveAction(keys[0]); +// const newState = responseCacheReducer(testState, action); +// expect(testState[keys[0]]).not.toBeUndefined(); +// expect(newState[keys[0]]).toBeUndefined(); +// }); +// +// it("shouldn't do anything when the specified key isn't cached", () => { +// const wrongKey = "this isn't cached"; +// const action = new ResponseCacheRemoveAction(wrongKey); +// const newState = responseCacheReducer(testState, action); +// expect(testState[wrongKey]).toBeUndefined(); +// expect(newState).toEqual(testState); +// }); +// }); +// +// describe("RESET_TIMESTAMPS", () => { +// const newTimeStamp = new Date().getTime(); +// const action = new ResetResponseCacheTimestampsAction(newTimeStamp); +// +// it("should perform the action without affecting the previous state", () => { +// //testState has already been frozen above +// responseCacheReducer(testState, action); +// }); +// +// it("should set the timestamp of all requests in the cache", () => { +// const newState = responseCacheReducer(testState, action); +// Object.keys(newState).forEach((key) => { +// expect(newState[key].timeAdded).toEqual(newTimeStamp); +// }); +// }); +// +// }); +// +// +// }); diff --git a/src/app/core/cache/response-cache.reducer.ts b/src/app/core/cache/response-cache.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e0fa6f5eb47c1b173b05c397f5465f7d566da49 --- /dev/null +++ b/src/app/core/cache/response-cache.reducer.ts @@ -0,0 +1,112 @@ +import { + ResponseCacheAction, ResponseCacheActionTypes, + ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction, + ResponseCacheAddAction +} from "./response-cache.actions"; +import { CacheEntry } from "./cache-entry"; +import { hasValue } from "../../shared/empty.util"; +import { Response } from "./response-cache.models"; + +/** + * An entry in the ResponseCache + */ +export class ResponseCacheEntry implements CacheEntry { + key: string; + response: Response; + timeAdded: number; + msToLive: number; +} + +/** + * The ResponseCache State + */ +export interface ResponseCacheState { + [key: string]: ResponseCacheEntry +} + +// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) +const initialState = Object.create(null); + +/** + * The ResponseCache Reducer + * + * @param state + * the current state + * @param action + * the action to perform on the state + * @return ResponseCacheState + * the new state + */ +export const responseCacheReducer = (state = initialState, action: ResponseCacheAction): ResponseCacheState => { + switch (action.type) { + + case ResponseCacheActionTypes.ADD: { + return addToCache(state, <ResponseCacheAddAction> action); + } + + case ResponseCacheActionTypes.REMOVE: { + return removeFromCache(state, <ResponseCacheRemoveAction> action); + } + + case ResponseCacheActionTypes.RESET_TIMESTAMPS: { + return resetResponseCacheTimestamps(state, <ResetResponseCacheTimestampsAction>action) + } + + default: { + return state; + } + } +}; + +function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState { + return Object.assign({}, state, { + [action.payload.key]: { + key: action.payload.key, + response: action.payload.response, + timeAdded: action.payload.timeAdded, + msToLive: action.payload.msToLive + } + }); +} + +/** + * Remove a request from the cache + * + * @param state + * the current state + * @param action + * an ResponseCacheRemoveAction + * @return ResponseCacheState + * the new state, with the request removed if it existed. + */ +function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState { + if (hasValue(state[action.payload])) { + let newCache = Object.assign({}, state); + delete newCache[action.payload]; + + return newCache; + } + else { + return state; + } +} + +/** + * Set the timeAdded timestamp of every cached request to the specified value + * + * @param state + * the current state + * @param action + * a ResetResponseCacheTimestampsAction + * @return ResponseCacheState + * the new state, with all timeAdded timestamps set to the specified value + */ +function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState { + let newState = Object.create(null); + Object.keys(state).forEach(key => { + newState[key] = Object.assign({}, state[key], { + timeAdded: action.payload + }); + }); + return newState; +} diff --git a/src/app/core/cache/response-cache.service.spec.ts b/src/app/core/cache/response-cache.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec9da670a1a2233eba437332317af64092c81308 --- /dev/null +++ b/src/app/core/cache/response-cache.service.spec.ts @@ -0,0 +1,146 @@ +import { ResponseCacheService } from "./response-cache.service"; +import { Store } from "@ngrx/store"; +import { ResponseCacheState, ResponseCacheEntry } from "./response-cache.reducer"; +import { OpaqueToken } from "@angular/core"; +import { Observable } from "rxjs"; + +// describe("ResponseCacheService", () => { +// let service: ResponseCacheService; +// let store: Store<ResponseCacheState>; +// +// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"]; +// const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')]; +// const resourceID = "9978"; +// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 }; +// const sortOptions = { "field": "id", "direction": 0 }; +// const timestamp = new Date().getTime(); +// const validCacheEntry = (key) => { +// return { +// key: key, +// timeAdded: timestamp, +// msToLive: 24 * 60 * 60 * 1000 // a day +// } +// }; +// const invalidCacheEntry = (key) => { +// return { +// key: key, +// timeAdded: 0, +// msToLive: 0 +// } +// }; +// +// beforeEach(() => { +// store = new Store<ResponseCacheState>(undefined, undefined, undefined); +// spyOn(store, 'dispatch'); +// service = new ResponseCacheService(store); +// spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp }); +// }); +// +// describe("findAll", () => { +// beforeEach(() => { +// spyOn(service, "get").and.callFake((key) => Observable.of({key: key})); +// }); +// describe("if the key isn't cached", () => { +// beforeEach(() => { +// spyOn(service, "has").and.returnValue(false); +// }); +// it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => { +// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions); +// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions)) +// }); +// it("should return an observable of the newly cached request with the specified key", () => { +// let result: ResponseCacheEntry; +// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry); +// expect(result.key).toEqual(keys[0]); +// }); +// }); +// describe("if the key is already cached", () => { +// beforeEach(() => { +// spyOn(service, "has").and.returnValue(true); +// }); +// it("shouldn't dispatch anything", () => { +// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions); +// expect(store.dispatch).not.toHaveBeenCalled(); +// }); +// it("should return an observable of the existing cached request with the specified key", () => { +// let result: ResponseCacheEntry; +// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry); +// expect(result.key).toEqual(keys[0]); +// }); +// }); +// }); +// +// describe("findById", () => { +// beforeEach(() => { +// spyOn(service, "get").and.callFake((key) => Observable.of({key: key})); +// }); +// describe("if the key isn't cached", () => { +// beforeEach(() => { +// spyOn(service, "has").and.returnValue(false); +// }); +// it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => { +// service.findById(keys[0], serviceTokens[0], resourceID); +// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindByIDAction(keys[0], serviceTokens[0], resourceID)) +// }); +// it("should return an observable of the newly cached request with the specified key", () => { +// let result: ResponseCacheEntry; +// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry); +// expect(result.key).toEqual(keys[0]); +// }); +// }); +// describe("if the key is already cached", () => { +// beforeEach(() => { +// spyOn(service, "has").and.returnValue(true); +// }); +// it("shouldn't dispatch anything", () => { +// service.findById(keys[0], serviceTokens[0], resourceID); +// expect(store.dispatch).not.toHaveBeenCalled(); +// }); +// it("should return an observable of the existing cached request with the specified key", () => { +// let result: ResponseCacheEntry; +// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry); +// expect(result.key).toEqual(keys[0]); +// }); +// }); +// }); +// +// describe("get", () => { +// it("should return an observable of the cached request with the specified key", () => { +// spyOn(store, "select").and.callFake((...args:Array<any>) => { +// return Observable.of(validCacheEntry(args[args.length - 1])); +// }); +// +// let testObj: ResponseCacheEntry; +// service.get(keys[1]).take(1).subscribe(entry => testObj = entry); +// expect(testObj.key).toEqual(keys[1]); +// }); +// +// it("should not return a cached request that has exceeded its time to live", () => { +// spyOn(store, "select").and.callFake((...args:Array<any>) => { +// return Observable.of(invalidCacheEntry(args[args.length - 1])); +// }); +// +// let getObsHasFired = false; +// const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true); +// expect(getObsHasFired).toBe(false); +// subscription.unsubscribe(); +// }); +// }); +// +// describe("has", () => { +// it("should return true if the request with the supplied key is cached and still valid", () => { +// spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1]))); +// expect(service.has(keys[1])).toBe(true); +// }); +// +// it("should return false if the request with the supplied key isn't cached", () => { +// spyOn(store, 'select').and.returnValue(Observable.of(undefined)); +// expect(service.has(keys[1])).toBe(false); +// }); +// +// it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => { +// spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1]))); +// expect(service.has(keys[1])).toBe(false); +// }); +// }); +// }); diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..17d9ed10913b35e8829ad3095c13f29f681df124 --- /dev/null +++ b/src/app/core/cache/response-cache.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { + ResponseCacheState, ResponseCacheEntry +} from "./response-cache.reducer"; +import { Observable } from "rxjs"; +import { hasNoValue } from "../../shared/empty.util"; +import { + ResponseCacheRemoveAction, + ResponseCacheAddAction +} from "./response-cache.actions"; +import { Response } from "./response-cache.models"; + +/** + * A service to interact with the response cache + */ +@Injectable() +export class ResponseCacheService { + constructor( + private store: Store<ResponseCacheState> + ) {} + + add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> { + if (!this.has(key)) { + // this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions)); + this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive)); + } + return this.get(key); + } + + /** + * Get an observable of the response with the specified key + * + * @param key + * the key of the response to get + * @return Observable<ResponseCacheEntry> + * an observable of the ResponseCacheEntry with the specified key + */ + get(key: string): Observable<ResponseCacheEntry> { + return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key) + .filter(entry => this.isValid(entry)) + .distinctUntilChanged() + } + + /** + * Check whether the response with the specified key is cached + * + * @param key + * the key of the response to check + * @return boolean + * true if the response with the specified key is cached, + * false otherwise + */ + has(key: string): boolean { + let result: boolean; + + this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key) + .take(1) + .subscribe(entry => { + result = this.isValid(entry); + }); + + return result; + } + + /** + * Check whether a ResponseCacheEntry should still be cached + * + * @param entry + * the entry to check + * @return boolean + * false if the entry is null, undefined, or its time to + * live has been exceeded, true otherwise + */ + private isValid(entry: ResponseCacheEntry): boolean { + if (hasNoValue(entry)) { + return false; + } + else { + const timeOutdated = entry.timeAdded + entry.msToLive; + const isOutDated = new Date().getTime() > timeOutdated; + if (isOutDated) { + this.store.dispatch(new ResponseCacheRemoveAction(entry.key)); + } + return !isOutDated; + } + } + +} diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index b2d6c95ad5dde257b39f6127b1374610a7cc2407..ef9da245dfc030e7f6671842163d48489190315b 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,12 +1,11 @@ import { EffectsModule } from "@ngrx/effects"; -import { CollectionDataEffects } from "./data-services/collection-data.effects"; -import { ItemDataEffects } from "./data-services/item-data.effects"; -import { ObjectCacheEffects } from "./data-services/object-cache.effects"; -import { RequestCacheEffects } from "./data-services/request-cache.effects"; +import { ObjectCacheEffects } from "./data/object-cache.effects"; +import { RequestCacheEffects } from "./data/request-cache.effects"; +import { HrefIndexEffects } from "./index/href-index.effects"; +import { RequestEffects } from "./data/request.effects"; export const coreEffects = [ - EffectsModule.run(CollectionDataEffects), - EffectsModule.run(ItemDataEffects), - EffectsModule.run(RequestCacheEffects), + EffectsModule.run(RequestEffects), EffectsModule.run(ObjectCacheEffects), + EffectsModule.run(HrefIndexEffects), ]; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5cc690ed80eb87ad9801acbea88c9d380923fb13..cdd474a909224b193d121441a9c772947a3c148d 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,15 +1,18 @@ import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; - import { SharedModule } from "../shared/shared.module"; + import { isNotEmpty } from "../shared/empty.util"; import { FooterComponent } from "./footer/footer.component"; import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service"; import { ObjectCacheService } from "./cache/object-cache.service"; -import { RequestCacheService } from "./cache/request-cache.service"; -import { CollectionDataService } from "./data-services/collection-data.service"; -import { ItemDataService } from "./data-services/item-data.service"; -import { PaginationOptions } from "./shared/pagination-options.model"; +import { ResponseCacheService } from "./cache/response-cache.service"; +import { CollectionDataService } from "./data/collection-data.service"; +import { ItemDataService } from "./data/item-data.service"; +import { RequestService } from "./data/request.service"; +import { RemoteDataBuildService } from "./cache/builders/remote-data-build.service"; +import { CommunityDataService } from "./data/community-data.service"; +import { PaginationOptions } from "./cache/models/pagination-options.model"; const IMPORTS = [ CommonModule, @@ -25,12 +28,14 @@ const EXPORTS = [ ]; const PROVIDERS = [ + CommunityDataService, CollectionDataService, ItemDataService, DSpaceRESTv2Service, ObjectCacheService, PaginationOptions, - RequestCacheService + RequestService, + RemoteDataBuildService ]; @NgModule({ diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 71f25ee0b0c0c648999e4fa66bf37b5a981331f7..556866dbc4157db0e8da02c4e6a7143c7c23487f 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -1,12 +1,18 @@ import { combineReducers } from "@ngrx/store"; import { CacheState, cacheReducer } from "./cache/cache.reducers"; +import { IndexState, indexReducer } from "./index/index.reducers"; +import { DataState, dataReducer } from "./data/data.reducers"; export interface CoreState { - cache: CacheState + cache: CacheState, + index: IndexState, + data: DataState } export const reducers = { - cache: cacheReducer + cache: cacheReducer, + index: indexReducer, + data: dataReducer }; export function coreReducer(state: any, action: any) { diff --git a/src/app/core/data-services/collection-data.effects.ts b/src/app/core/data-services/collection-data.effects.ts deleted file mode 100644 index 9586940defe82196a9d4bf4abef82db77f155dd8..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/collection-data.effects.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Inject, Injectable } from "@angular/core"; -import { DataEffects } from "./data.effects"; -import { Serializer } from "../serializer"; -import { Collection } from "../shared/collection.model"; -import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; -import { Actions, Effect } from "@ngrx/effects"; -import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions"; -import { CollectionDataService } from "./collection-data.service"; - -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; - -@Injectable() -export class CollectionDataEffects extends DataEffects<Collection> { - constructor( - @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig, - actions$: Actions, - restApi: DSpaceRESTv2Service, - cache: ObjectCacheService, - dataService: CollectionDataService - ) { - super(EnvConfig, actions$, restApi, cache, dataService); - } - - protected getFindAllEndpoint(action: RequestCacheFindAllAction): string { - return '/collections'; - } - - protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string { - return `/collections/${action.payload.resourceID}`; - } - - protected getSerializer(): Serializer<Collection> { - return new DSpaceRESTv2Serializer(Collection); - } - - @Effect() findAll$ = this.findAll; - - @Effect() findById$ = this.findById; -} diff --git a/src/app/core/data-services/collection-data.service.ts b/src/app/core/data-services/collection-data.service.ts deleted file mode 100644 index cc850900db4c0d5616787145f09d922de6dd51d9..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/collection-data.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable, OpaqueToken } from "@angular/core"; -import { DataService } from "./data.service"; -import { Collection } from "../shared/collection.model"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { RequestCacheService } from "../cache/request-cache.service"; - -@Injectable() -export class CollectionDataService extends DataService<Collection> { - serviceName = new OpaqueToken('CollectionDataService'); - - constructor( - protected objectCache: ObjectCacheService, - protected requestCache: RequestCacheService, - ) { - super(Collection); - } - -} diff --git a/src/app/core/data-services/data.effects.ts b/src/app/core/data-services/data.effects.ts deleted file mode 100644 index 107ad7eca3a4826778044bc6c01c6fe7a4c8421f..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/data.effects.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Inject } from "@angular/core"; -import { Actions } from "@ngrx/effects"; -import { Observable } from "rxjs"; -import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model"; -import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { CacheableObject } from "../cache/object-cache.reducer"; -import { Serializer } from "../serializer"; -import { - RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction, - RequestCacheErrorAction, RequestCacheFindByIDAction -} from "../cache/request-cache.actions"; -import { DataService } from "./data.service"; -import { hasNoValue } from "../../shared/empty.util"; - -import { GlobalConfig } from '../../../config'; - -export abstract class DataEffects<T extends CacheableObject> { - protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string; - protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string; - protected abstract getSerializer(): Serializer<T>; - - constructor( - private EnvConfig: GlobalConfig, - private actions$: Actions, - private restApi: DSpaceRESTv2Service, - private objectCache: ObjectCacheService, - private dataService: DataService<T> - ) { } - - // TODO, results of a findall aren't retrieved from cache yet - protected findAll = this.actions$ - .ofType(RequestCacheActionTypes.FIND_ALL) - .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName) - .flatMap((action: RequestCacheFindAllAction) => { - //TODO scope, pagination, sorting -> when we know how that works in rest - return this.restApi.get(this.getFindAllEndpoint(action)) - .map((data: DSpaceRESTV2Response) => this.getSerializer().deserializeArray(data)) - .do((ts: T[]) => { - ts.forEach((t) => { - if (hasNoValue(t) || hasNoValue(t.uuid)) { - throw new Error('The server returned an invalid object'); - } - this.objectCache.add(t, this.EnvConfig.cache.msToLive); - }); - }) - .map((ts: Array<T>) => ts.map(t => t.uuid)) - .map((ids: Array<string>) => new RequestCacheSuccessAction(action.payload.key, ids, new Date().getTime(), this.EnvConfig.cache.msToLive)) - .catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message))); - }); - - protected findById = this.actions$ - .ofType(RequestCacheActionTypes.FIND_BY_ID) - .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName) - .flatMap((action: RequestCacheFindByIDAction) => { - return this.restApi.get(this.getFindByIdEndpoint(action)) - .map((data: DSpaceRESTV2Response) => this.getSerializer().deserialize(data)) - .do((t: T) => { - if (hasNoValue(t) || hasNoValue(t.uuid)) { - throw new Error('The server returned an invalid object'); - } - this.objectCache.add(t, this.EnvConfig.cache.msToLive); - }) - .map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], new Date().getTime(), this.EnvConfig.cache.msToLive)) - .catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message))); - }); - -} diff --git a/src/app/core/data-services/data.service.ts b/src/app/core/data-services/data.service.ts deleted file mode 100644 index ddbfa03eb44009262e3483031032c4649c0260fd..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/data.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { OpaqueToken } from "@angular/core"; -import { Observable } from "rxjs"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { RequestCacheService } from "../cache/request-cache.service"; -import { CacheableObject } from "../cache/object-cache.reducer"; -import { ParamHash } from "../shared/param-hash"; -import { isNotEmpty } from "../../shared/empty.util"; -import { GenericConstructor } from "../shared/generic-constructor"; -import { RemoteData } from "./remote-data"; - -export abstract class DataService<T extends CacheableObject> { - abstract serviceName: OpaqueToken; - protected abstract objectCache: ObjectCacheService; - protected abstract requestCache: RequestCacheService; - - constructor(private modelType: GenericConstructor<T>) { - - } - - findAll(scopeID?: string): RemoteData<Array<T>> { - const key = new ParamHash(this.serviceName, 'findAll', scopeID).toString(); - const requestCacheObs = this.requestCache.findAll(key, this.serviceName, scopeID); - return new RemoteData( - requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(), - requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(), - requestCacheObs - .map(entry => entry.resourceUUIDs) - .flatMap((resourceUUIDs: Array<string>) => { - // use those IDs to fetch the actual objects from the ObjectCache - return this.objectCache.getList<T>(resourceUUIDs, this.modelType); - }).distinctUntilChanged() - ); - } - - findById(id: string): RemoteData<T> { - const key = new ParamHash(this.serviceName, 'findById', id).toString(); - const requestCacheObs = this.requestCache.findById(key, this.serviceName, id); - return new RemoteData( - requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(), - requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(), - requestCacheObs - .map(entry => entry.resourceUUIDs) - .flatMap((resourceUUIDs: Array<string>) => { - if (isNotEmpty(resourceUUIDs)) { - return this.objectCache.get<T>(resourceUUIDs[0], this.modelType); - } - else { - return Observable.of(undefined); - } - }).distinctUntilChanged() - ); - } - -} diff --git a/src/app/core/data-services/item-data.effects.ts b/src/app/core/data-services/item-data.effects.ts deleted file mode 100644 index 8c140c8398b3fa0765899020ad7f3d57c6731dc9..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/item-data.effects.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Inject, Injectable } from "@angular/core"; -import { DataEffects } from "./data.effects"; -import { Serializer } from "../serializer"; -import { Item } from "../shared/item.model"; -import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; -import { Actions, Effect } from "@ngrx/effects"; -import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions"; -import { ItemDataService } from "./item-data.service"; - -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; - -@Injectable() -export class ItemDataEffects extends DataEffects<Item> { - constructor( - @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig, - actions$: Actions, - restApi: DSpaceRESTv2Service, - cache: ObjectCacheService, - dataService: ItemDataService - ) { - super(EnvConfig, actions$, restApi, cache, dataService); - } - - protected getFindAllEndpoint(action: RequestCacheFindAllAction): string { - return '/items'; - } - - protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string { - return `/items/${action.payload.resourceID}`; - } - - protected getSerializer(): Serializer<Item> { - return new DSpaceRESTv2Serializer(Item); - } - - @Effect() findAll$ = this.findAll; - - @Effect() findById$ = this.findById; -} diff --git a/src/app/core/data-services/item-data.service.ts b/src/app/core/data-services/item-data.service.ts deleted file mode 100644 index f3c8fd83af184e7ac5c9a95314cf446fc567123e..0000000000000000000000000000000000000000 --- a/src/app/core/data-services/item-data.service.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Injectable, OpaqueToken } from "@angular/core"; -import { DataService } from "./data.service"; -import { Item } from "../shared/item.model"; -import { ObjectCacheService } from "../cache/object-cache.service"; -import { RequestCacheService } from "../cache/request-cache.service"; - -@Injectable() -export class ItemDataService extends DataService<Item> { - serviceName = new OpaqueToken('ItemDataService'); - - constructor( - protected objectCache: ObjectCacheService, - protected requestCache: RequestCacheService, - ) { - super(Item); - } - -} diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..232345d2be1e963fff3f1486439753847e753470 --- /dev/null +++ b/src/app/core/data/collection-data.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@angular/core"; +import { DataService } from "./data.service"; +import { Collection } from "../shared/collection.model"; +import { ObjectCacheService } from "../cache/object-cache.service"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { Store } from "@ngrx/store"; +import { NormalizedCollection } from "../cache/models/normalized-collection.model"; +import { CoreState } from "../core.reducers"; +import { RequestService } from "./request.service"; +import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service"; + +@Injectable() +export class CollectionDataService extends DataService<NormalizedCollection, Collection> { + protected endpoint = '/collections'; + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store<CoreState> + ) { + super(NormalizedCollection); + } + +} diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e635974071dc504154787b6ec6d4d73afcf9e41 --- /dev/null +++ b/src/app/core/data/community-data.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@angular/core"; +import { DataService } from "./data.service"; +import { Community } from "../shared/community.model"; +import { ObjectCacheService } from "../cache/object-cache.service"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { Store } from "@ngrx/store"; +import { NormalizedCommunity } from "../cache/models/normalized-community.model"; +import { CoreState } from "../core.reducers"; +import { RequestService } from "./request.service"; +import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service"; + +@Injectable() +export class CommunityDataService extends DataService<NormalizedCommunity, Community> { + protected endpoint = '/communities'; + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store<CoreState> + ) { + super(NormalizedCommunity); + } + +} diff --git a/src/app/core/data/data.reducers.ts b/src/app/core/data/data.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..af7d2697ccee4fa2dc3d1eae8b097c1ad71512c7 --- /dev/null +++ b/src/app/core/data/data.reducers.ts @@ -0,0 +1,14 @@ +import { combineReducers } from "@ngrx/store"; +import { RequestState, requestReducer } from "./request.reducer"; + +export interface DataState { + request: RequestState +} + +export const reducers = { + request: requestReducer +}; + +export function dataReducer(state: any, action: any) { + return combineReducers(reducers)(state, action); +} diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..809ff799b3e74c14ef43d9cc1ac13f52f8616531 --- /dev/null +++ b/src/app/core/data/data.service.ts @@ -0,0 +1,70 @@ +import { ObjectCacheService } from "../cache/object-cache.service"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { CacheableObject } from "../cache/object-cache.reducer"; +import { hasValue } from "../../shared/empty.util"; +import { RemoteData } from "./remote-data"; +import { FindAllRequest, FindByIDRequest, Request } from "./request.models"; +import { Store } from "@ngrx/store"; +import { RequestConfigureAction, RequestExecuteAction } from "./request.actions"; +import { CoreState } from "../core.reducers"; +import { RequestService } from "./request.service"; +import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service"; +import { GenericConstructor } from "../shared/generic-constructor"; + +export abstract class DataService<TNormalized extends CacheableObject, TDomain> { + protected abstract objectCache: ObjectCacheService; + protected abstract responseCache: ResponseCacheService; + protected abstract requestService: RequestService; + protected abstract rdbService: RemoteDataBuildService; + protected abstract store: Store<CoreState>; + protected abstract endpoint: string; + + constructor(private normalizedResourceType: GenericConstructor<TNormalized>) { + + } + + protected getFindAllHref(scopeID?): string { + let result = this.endpoint; + if (hasValue(scopeID)) { + result += `?scope=${scopeID}` + } + return result; + } + + findAll(scopeID?: string): RemoteData<Array<TDomain>> { + const href = this.getFindAllHref(scopeID); + if (!this.responseCache.has(href) && !this.requestService.isPending(href)) { + const request = new FindAllRequest(href, this.normalizedResourceType, scopeID); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType); + // return this.rdbService.buildList(href); + } + + protected getFindByIDHref(resourceID): string { + return `${this.endpoint}/${resourceID}`; + } + + findById(id: string): RemoteData<TDomain> { + const href = this.getFindByIDHref(id); + if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) { + const request = new FindByIDRequest(href, this.normalizedResourceType, id); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType); + // return this.rdbService.buildSingle(href); + } + + findByHref(href: string): RemoteData<TDomain> { + if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) { + const request = new Request(href, this.normalizedResourceType); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType); + // return this.rdbService.buildSingle(href)); + } + +} diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc13999f37f376df06279a9c8ea656ee551bdaf3 --- /dev/null +++ b/src/app/core/data/item-data.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@angular/core"; +import { DataService } from "./data.service"; +import { Item } from "../shared/item.model"; +import { ObjectCacheService } from "../cache/object-cache.service"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { Store } from "@ngrx/store"; +import { CoreState } from "../core.reducers"; +import { NormalizedItem } from "../cache/models/normalized-item.model"; +import { RequestService } from "./request.service"; +import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service"; + +@Injectable() +export class ItemDataService extends DataService<NormalizedItem, Item> { + protected endpoint = '/items'; + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store<CoreState> +) { + super(NormalizedItem); + } +} diff --git a/src/app/core/data-services/object-cache.effects.ts b/src/app/core/data/object-cache.effects.ts similarity index 81% rename from src/app/core/data-services/object-cache.effects.ts rename to src/app/core/data/object-cache.effects.ts index 26f13ea1b51f5ed69e080a4a7640b71697092605..af5a0658a3fb8d82c50b91b4f47f527a00e47999 100644 --- a/src/app/core/data-services/object-cache.effects.ts +++ b/src/app/core/data/object-cache.effects.ts @@ -2,15 +2,12 @@ import { Injectable } from "@angular/core"; import { Actions, Effect } from "@ngrx/effects"; import { StoreActionTypes } from "../../store.actions"; import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions"; -import { Store } from "@ngrx/store"; -import { ObjectCacheState } from "../cache/object-cache.reducer"; @Injectable() export class ObjectCacheEffects { constructor( - private actions$: Actions, - private store: Store<ObjectCacheState> + private actions$: Actions ) { } /** diff --git a/src/app/core/data-services/remote-data.ts b/src/app/core/data/remote-data.ts similarity index 73% rename from src/app/core/data-services/remote-data.ts rename to src/app/core/data/remote-data.ts index 1b9ff177ef317a6458b8dea4f900481dd6eeb38e..7fa02bf25c0908bd38b95fe9b38d03f672e30434 100644 --- a/src/app/core/data-services/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -1,8 +1,6 @@ import { Observable } from "rxjs"; -import { hasValue } from "../../shared/empty.util"; export enum RemoteDataState { - //TODO RequestPending will never happen: implement it in the store & DataEffects. RequestPending, ResponsePending, Failed, @@ -10,12 +8,14 @@ export enum RemoteDataState { } /** - * A class to represent the state of + * A class to represent the state of a remote resource */ export class RemoteData<T> { - constructor( - private storeLoading: Observable<boolean>, + public self: string, + private requestPending: Observable<boolean>, + private responsePending: Observable<boolean>, + private isSuccessFul: Observable<boolean>, public errorMessage: Observable<string>, public payload: Observable<T> ) { @@ -23,13 +23,17 @@ export class RemoteData<T> { get state(): Observable<RemoteDataState> { return Observable.combineLatest( - this.storeLoading, - this.errorMessage.map(msg => hasValue(msg)), - (storeLoading, hasMsg) => { - if (storeLoading) { + this.requestPending, + this.responsePending, + this.isSuccessFul, + (requestPending, responsePending, isSuccessFul) => { + if (requestPending) { + return RemoteDataState.RequestPending + } + else if (responsePending) { return RemoteDataState.ResponsePending } - else if (hasMsg) { + else if (!isSuccessFul) { return RemoteDataState.Failed } else { diff --git a/src/app/core/data-services/request-cache.effects.ts b/src/app/core/data/request-cache.effects.ts similarity index 72% rename from src/app/core/data-services/request-cache.effects.ts rename to src/app/core/data/request-cache.effects.ts index b8dde5115975c2dd8510239b484d73a9c2b2ec86..3c650d95f133042e73915dbbfa9924330593cb55 100644 --- a/src/app/core/data-services/request-cache.effects.ts +++ b/src/app/core/data/request-cache.effects.ts @@ -1,16 +1,15 @@ -import { Injectable } from "@angular/core"; +import { Injectable, Inject } from "@angular/core"; import { Actions, Effect } from "@ngrx/effects"; -import { ResetRequestCacheTimestampsAction } from "../cache/request-cache.actions"; -import { Store } from "@ngrx/store"; -import { RequestCacheState } from "../cache/request-cache.reducer"; import { ObjectCacheActionTypes } from "../cache/object-cache.actions"; +import { GlobalConfig, GLOBAL_CONFIG } from "../../../config"; +import { ResetResponseCacheTimestampsAction } from "../cache/response-cache.actions"; @Injectable() export class RequestCacheEffects { constructor( + @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, private actions$: Actions, - private store: Store<RequestCacheState> ) { } /** @@ -31,6 +30,5 @@ export class RequestCacheEffects { */ @Effect() fixTimestampsOnRehydrate = this.actions$ .ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS) - .map(() => new ResetRequestCacheTimestampsAction(new Date().getTime())); - + .map(() => new ResetResponseCacheTimestampsAction(new Date().getTime())); } diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..16ce1963bd789fae91f7a27ae33caf681b280d33 --- /dev/null +++ b/src/app/core/data/request.actions.ts @@ -0,0 +1,59 @@ +import { Action } from "@ngrx/store"; +import { type } from "../../shared/ngrx/type"; +import { CacheableObject } from "../cache/object-cache.reducer"; +import { Request } from "./request.models"; + +/** + * The list of RequestAction type definitions + */ +export const RequestActionTypes = { + CONFIGURE: type('dspace/core/data/request/CONFIGURE'), + EXECUTE: type('dspace/core/data/request/EXECUTE'), + COMPLETE: type('dspace/core/data/request/COMPLETE') +}; + +export class RequestConfigureAction implements Action { + type = RequestActionTypes.CONFIGURE; + payload: Request<CacheableObject>; + + constructor( + request: Request<CacheableObject> + ) { + this.payload = request; + } +} + +export class RequestExecuteAction implements Action { + type = RequestActionTypes.EXECUTE; + payload: string; + + constructor(key: string) { + this.payload = key + } +} + +/** + * An ngrx action to indicate a response was returned + */ +export class RequestCompleteAction implements Action { + type = RequestActionTypes.COMPLETE; + payload: string; + + /** + * Create a new RequestCompleteAction + * + * @param key + * the key under which this request is stored, + */ + constructor(key: string) { + this.payload = key; + } +} + +/** + * A type to encompass all RequestActions + */ +export type RequestAction + = RequestConfigureAction + | RequestExecuteAction + | RequestCompleteAction; diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5d887626ed5d8cfb2809aac1e14107bc226d326 --- /dev/null +++ b/src/app/core/data/request.effects.ts @@ -0,0 +1,71 @@ +import { Injectable, Inject } from "@angular/core"; +import { Actions, Effect } from "@ngrx/effects"; +import { Store } from "@ngrx/store"; +import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service"; +import { ObjectCacheService } from "../cache/object-cache.service"; +import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model"; +import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer"; +import { CacheableObject } from "../cache/object-cache.reducer"; +import { Observable } from "rxjs"; +import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models"; +import { hasNoValue } from "../../shared/empty.util"; +import { GlobalConfig, GLOBAL_CONFIG } from "../../../config"; +import { RequestState, RequestEntry } from "./request.reducer"; +import { + RequestActionTypes, RequestExecuteAction, + RequestCompleteAction +} from "./request.actions"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { RequestService } from "./request.service"; + +@Injectable() +export class RequestEffects { + + constructor( + @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, + private actions$: Actions, + private restApi: DSpaceRESTv2Service, + private objectCache: ObjectCacheService, + private responseCache: ResponseCacheService, + protected requestService: RequestService, + private store: Store<RequestState> + ) { } + + @Effect() execute = this.actions$ + .ofType(RequestActionTypes.EXECUTE) + .flatMap((action: RequestExecuteAction) => { + return this.requestService.get(action.payload) + .take(1); + }) + .flatMap((entry: RequestEntry) => { + const [ifArray, ifNotArray] = this.restApi.get(entry.request.href) + .share() // share ensures restApi.get() doesn't get called twice when the partitions are used below + .partition((data: DSpaceRESTV2Response) => Array.isArray(data._embedded)); + + return Observable.merge( + + ifArray.map((data: DSpaceRESTV2Response) => { + return new DSpaceRESTv2Serializer(entry.request.resourceType).deserializeArray(data); + }).do((cos: CacheableObject[]) => cos.forEach((t) => this.addToObjectCache(t))) + .map((cos: Array<CacheableObject>): Array<string> => cos.map(t => t.uuid)), + + ifNotArray.map((data: DSpaceRESTV2Response) => { + return new DSpaceRESTv2Serializer(entry.request.resourceType).deserialize(data); + }).do((co: CacheableObject) => this.addToObjectCache(co)) + .map((co: CacheableObject): Array<string> => [co.uuid]) + + ).map((ids: Array<string>) => new SuccessResponse(ids)) + .do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) + .map((response: Response) => new RequestCompleteAction(entry.request.href)) + .catch((error: Error) => Observable.of(new ErrorResponse(error.message)) + .do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) + .map((response: Response) => new RequestCompleteAction(entry.request.href))); + }); + + protected addToObjectCache(co: CacheableObject): void { + if (hasNoValue(co) || hasNoValue(co.uuid)) { + throw new Error('The server returned an invalid object'); + } + this.objectCache.add(co, this.EnvConfig.cache.msToLive); + } +} diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts new file mode 100644 index 0000000000000000000000000000000000000000..9171bbe509811c0cfa80d646798e2c17198afc8a --- /dev/null +++ b/src/app/core/data/request.models.ts @@ -0,0 +1,32 @@ +import { SortOptions } from "../cache/models/sort-options.model"; +import { PaginationOptions } from "../cache/models/pagination-options.model"; +import { GenericConstructor } from "../shared/generic-constructor"; + +export class Request<T> { + constructor( + public href: string, + public resourceType: GenericConstructor<T> + ) {} +} + +export class FindByIDRequest<T> extends Request<T> { + constructor( + href: string, + resourceType: GenericConstructor<T>, + public resourceID: string + ) { + super(href, resourceType); + } +} + +export class FindAllRequest<T> extends Request<T> { + constructor( + href: string, + resourceType: GenericConstructor<T>, + public scopeID?: string, + public paginationOptions?: PaginationOptions, + public sortOptions?: SortOptions + ) { + super(href, resourceType); + } +} diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..e20accc8317d11851f217c00474c8c7b4ac4854d --- /dev/null +++ b/src/app/core/data/request.reducer.ts @@ -0,0 +1,81 @@ +import { CacheableObject } from "../cache/object-cache.reducer"; +import { + RequestActionTypes, RequestAction, RequestConfigureAction, + RequestExecuteAction, RequestCompleteAction +} from "./request.actions"; +import { Request } from "./request.models"; + +export class RequestEntry { + request: Request<CacheableObject>; + requestPending: boolean; + responsePending: boolean; + completed: boolean; +} + + +export interface RequestState { + [key: string]: RequestEntry +} + +// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) +const initialState = Object.create(null); + +export const requestReducer = (state = initialState, action: RequestAction): RequestState => { + switch (action.type) { + + case RequestActionTypes.CONFIGURE: { + return configureRequest(state, <RequestConfigureAction> action); + } + + case RequestActionTypes.EXECUTE: { + return executeRequest(state, <RequestExecuteAction> action); + } + + case RequestActionTypes.COMPLETE: { + return completeRequest(state, <RequestCompleteAction> action); + } + + default: { + return state; + } + } +}; + +function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState { + return Object.assign({}, state, { + [action.payload.href]: { + request: action.payload, + requestPending: true, + responsePending: false, + completed: false + } + }); +} + +function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState { + return Object.assign({}, state, { + [action.payload]: Object.assign({}, state[action.payload], { + requestPending: false, + responsePending: true + }) + }); +} + +/** + * Update a request with the response + * + * @param state + * the current state + * @param action + * a RequestCompleteAction + * @return RequestState + * the new state, with the response added to the request + */ +function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState { + return Object.assign({}, state, { + [action.payload]: Object.assign({}, state[action.payload], { + responsePending: false, + completed: true + }) + }); +} diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3b28af2c236c101197a09c1a6ef2ca1dbc4a35a --- /dev/null +++ b/src/app/core/data/request.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from "@angular/core"; +import { RequestEntry, RequestState } from "./request.reducer"; +import { Store } from "@ngrx/store"; +import { Request } from "./request.models"; +import { hasValue } from "../../shared/empty.util"; +import { Observable } from "rxjs/Observable"; +import { RequestConfigureAction, RequestExecuteAction } from "./request.actions"; +import { ResponseCacheService } from "../cache/response-cache.service"; +import { ObjectCacheService } from "../cache/object-cache.service"; +import { CacheableObject } from "../cache/object-cache.reducer"; +import { GenericConstructor } from "../shared/generic-constructor"; + +@Injectable() +export class RequestService { + + constructor( + private objectCache: ObjectCacheService, + private responseCache: ResponseCacheService, + private store: Store<RequestState> + ) { + } + + isPending(href: string): boolean { + let isPending = false; + this.store.select<RequestEntry>('core', 'data', 'request', href) + .take(1) + .subscribe((re: RequestEntry) => { + isPending = (hasValue(re) && !re.completed) + }); + + return isPending; + } + + get(href: string): Observable<RequestEntry> { + return this.store.select<RequestEntry>('core', 'data', 'request', href); + } + + configure<T extends CacheableObject>(href: string, normalizedType: GenericConstructor<T>): void { + const isCached = this.objectCache.hasBySelfLink(href); + const isPending = this.isPending(href); + + if (!(isCached || isPending)) { + const request = new Request(href, normalizedType); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + } +} diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts index 2661b3708dce4fd70c4784150249c4889815fd10..236244873cf9a288a10f0fbedcec275e12aac00d 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts @@ -140,19 +140,20 @@ describe("DSpaceRESTv2Serializer", () => { describe("deserializeArray", () => { - it("should turn a valid document describing a collection of objects in to an array of valid models", () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const doc = { - "_embedded": testResponses - }; - - const models = serializer.deserializeArray(doc); - - expect(models[0].id).toBe(doc._embedded[0].id); - expect(models[0].name).toBe(doc._embedded[0].name); - expect(models[1].id).toBe(doc._embedded[1].id); - expect(models[1].name).toBe(doc._embedded[1].name); - }); + //TODO rewrite to incorporate normalisation. + // it("should turn a valid document describing a collection of objects in to an array of valid models", () => { + // const serializer = new DSpaceRESTv2Serializer(TestModel); + // const doc = { + // "_embedded": testResponses + // }; + // + // const models = serializer.deserializeArray(doc); + // + // expect(models[0].id).toBe(doc._embedded[0].id); + // expect(models[0].name).toBe(doc._embedded[0].name); + // expect(models[1].id).toBe(doc._embedded[1].id); + // expect(models[1].name).toBe(doc._embedded[1].name); + // }); //TODO cant implement/test this yet - depends on how relationships // will be handled in the rest api diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts index b5fa5983d8efbef770cca0bd56de25a60236677f..d4d5a7ce590b6bd6a537096137e7aad6061f20e6 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts @@ -55,7 +55,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> { if (Array.isArray(response._embedded)) { throw new Error('Expected a single model, use deserializeArray() instead'); } - return <T> Deserialize(response._embedded, this.modelType); + let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._embedded._links)); + return <T> Deserialize(normalized, this.modelType); } /** @@ -70,7 +71,26 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> { if (!Array.isArray(response._embedded)) { throw new Error('Expected an Array, use deserialize() instead'); } - return <Array<T>> Deserialize(response._embedded, this.modelType); + let normalized = response._embedded.map((resource) => { + return Object.assign({}, resource, this.normalizeLinks(resource._links)); + }); + + return <Array<T>> Deserialize(normalized, this.modelType); + } + + private normalizeLinks(links:any): any { + let normalizedLinks = links; + for (let link in normalizedLinks) { + if (Array.isArray(normalizedLinks[link])) { + normalizedLinks[link] = normalizedLinks[link].map(linkedResource => { + return linkedResource.href; + }); + } + else { + normalizedLinks[link] = normalizedLinks[link].href; + } + } + return normalizedLinks; } } diff --git a/src/app/core/footer/footer.component.spec.ts b/src/app/core/footer/footer.component.spec.ts index 1a4b26510bfd2c1286303460de5cd3ec07f19c4e..6015104003f1f551f0b9735ba659530767aba37d 100644 --- a/src/app/core/footer/footer.component.spec.ts +++ b/src/app/core/footer/footer.component.spec.ts @@ -10,7 +10,7 @@ import { DebugElement } from "@angular/core"; import { By } from '@angular/platform-browser'; -import { TranslateModule, TranslateLoader } from "ng2-translate"; +import { TranslateModule, TranslateLoader } from "@ngx-translate/core"; import { Store, StoreModule } from "@ngrx/store"; // Load the implementations that should be tested @@ -30,8 +30,10 @@ describe('Footer component', () => { beforeEach(async(() => { return TestBed.configureTestingModule({ imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({ - provide: TranslateLoader, - useClass: MockTranslateLoader + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } })], declarations: [FooterComponent], // declare the test component providers: [ diff --git a/src/app/core/index/href-index.actions.ts b/src/app/core/index/href-index.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c00f2d96cb7798e8bd63682440cc193f2860a4e --- /dev/null +++ b/src/app/core/index/href-index.actions.ts @@ -0,0 +1,58 @@ +import { Action } from "@ngrx/store"; +import { type } from "../../shared/ngrx/type"; + +/** + * The list of HrefIndexAction type definitions + */ +export const HrefIndexActionTypes = { + ADD: type('dspace/core/index/href/ADD'), + REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID') +}; + +/** + * An ngrx action to add an href to the index + */ +export class AddToHrefIndexAction implements Action { + type = HrefIndexActionTypes.ADD; + payload: { + href: string; + uuid: string; + }; + + /** + * Create a new AddToHrefIndexAction + * + * @param href + * the href to add + * @param uuid + * the uuid of the resource the href links to + */ + constructor(href: string, uuid: string) { + this.payload = { href, uuid }; + } +} + +/** + * An ngrx action to remove an href from the index + */ +export class RemoveUUIDFromHrefIndexAction implements Action { + type = HrefIndexActionTypes.REMOVE_UUID; + payload: string; + + /** + * Create a new RemoveUUIDFromHrefIndexAction + * + * @param uuid + * the uuid to remove all hrefs for + */ + constructor(uuid: string) { + this.payload = uuid; + } +} + +/** + * A type to encompass all HrefIndexActions + */ +export type HrefIndexAction + = AddToHrefIndexAction + | RemoveUUIDFromHrefIndexAction; diff --git a/src/app/core/index/href-index.effects.ts b/src/app/core/index/href-index.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e1c8ae8d1a7ce18c8e2cdb63b15d5a77f9c31e9 --- /dev/null +++ b/src/app/core/index/href-index.effects.ts @@ -0,0 +1,32 @@ +import { Injectable } from "@angular/core"; +import { Effect, Actions } from "@ngrx/effects"; +import { + ObjectCacheActionTypes, AddToObjectCacheAction, + RemoveFromObjectCacheAction +} from "../cache/object-cache.actions"; +import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from "./href-index.actions"; +import { hasValue } from "../../shared/empty.util"; + +@Injectable() +export class HrefIndexEffects { + + constructor( + private actions$: Actions + ) { } + + @Effect() add$ = this.actions$ + .ofType(ObjectCacheActionTypes.ADD) + .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self)) + .map((action: AddToObjectCacheAction) => { + return new AddToHrefIndexAction( + action.payload.objectToCache.self, + action.payload.objectToCache.uuid + ); + }); + + @Effect() remove$ = this.actions$ + .ofType(ObjectCacheActionTypes.REMOVE) + .map((action: RemoveFromObjectCacheAction) => { + return new RemoveUUIDFromHrefIndexAction(action.payload); + }); +} diff --git a/src/app/core/index/href-index.reducer.ts b/src/app/core/index/href-index.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cb46566df4d65eee08bb31b67691060b77f6ac8 --- /dev/null +++ b/src/app/core/index/href-index.reducer.ts @@ -0,0 +1,43 @@ +import { + HrefIndexAction, HrefIndexActionTypes, AddToHrefIndexAction, + RemoveUUIDFromHrefIndexAction +} from "./href-index.actions"; +export interface HrefIndexState { + [href: string]: string +} + +// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) +const initialState: HrefIndexState = Object.create(null); + +export const hrefIndexReducer = (state = initialState, action: HrefIndexAction): HrefIndexState => { + switch (action.type) { + + case HrefIndexActionTypes.ADD: { + return addToHrefIndex(state, <AddToHrefIndexAction>action); + } + + case HrefIndexActionTypes.REMOVE_UUID: { + return removeUUIDFromHrefIndex(state, <RemoveUUIDFromHrefIndexAction>action) + } + + default: { + return state; + } + } +}; + +function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState { + return Object.assign({}, state, { + [action.payload.href]: action.payload.uuid + }); +} + +function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState { + let newState = Object.create(null); + for (let href in state) { + if (state[href] !== action.payload) { + newState[href] = state[href]; + } + } + return newState; +} diff --git a/src/app/core/index/index.reducers.ts b/src/app/core/index/index.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7e3d7218a2d87f70d7fe7a69aa95a06121d381d --- /dev/null +++ b/src/app/core/index/index.reducers.ts @@ -0,0 +1,14 @@ +import { combineReducers } from "@ngrx/store"; +import { HrefIndexState, hrefIndexReducer } from "./href-index.reducer"; + +export interface IndexState { + href: HrefIndexState +} + +export const reducers = { + href: hrefIndexReducer +}; + +export function indexReducer(state: any, action: any) { + return combineReducers(reducers)(state, action); +} diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 43217a1292fe1b1fb420021ac0700d8c77b83200..5325e395d830a4fc4bc1eb32b18fbfaf15f2b520 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,8 +1,7 @@ -import { inheritSerialization } from "cerialize"; import { DSpaceObject } from "./dspace-object.model"; import { Bundle } from "./bundle.model"; +import { RemoteData } from "../data/remote-data"; -@inheritSerialization(DSpaceObject) export class Bitstream extends DSpaceObject { /** @@ -28,10 +27,16 @@ export class Bitstream extends DSpaceObject { /** * An array of Bundles that are direct parents of this Bitstream */ - parents: Array<Bundle>; + parents: Array<RemoteData<Bundle>>; /** * The Bundle that owns this Bitstream */ owner: Bundle; + + /** + * The Bundle that owns this Bitstream + */ + retrieve: string; + } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index b990c8617e0022070523c0cde798b2407cc2c503..7c2f6b05d44df5bcba3bb69927031ed240a19117 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,23 +1,24 @@ -import { inheritSerialization } from "cerialize"; import { DSpaceObject } from "./dspace-object.model"; import { Bitstream } from "./bitstream.model"; import { Item } from "./item.model"; +import { RemoteData } from "../data/remote-data"; -@inheritSerialization(DSpaceObject) export class Bundle extends DSpaceObject { /** * The primary bitstream of this Bundle */ - primaryBitstream: Bitstream; + primaryBitstream: RemoteData<Bitstream>; /** * An array of Items that are direct parents of this Bundle */ - parents: Array<Item>; + parents: Array<RemoteData<Item>>; /** * The Item that owns this Bundle */ owner: Item; + bitstreams: Array<RemoteData<Bitstream>> + } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 7048ded4a4273b748553cbf3f330f20f5bb06570..4287eff63cdcb768a268e56a7f4289cb3adf02b2 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,14 +1,13 @@ -import { autoserialize, inheritSerialization } from "cerialize"; import { DSpaceObject } from "./dspace-object.model"; import { Bitstream } from "./bitstream.model"; +import { Item } from "./item.model"; +import { RemoteData } from "../data/remote-data"; -@inheritSerialization(DSpaceObject) export class Collection extends DSpaceObject { /** * A string representing the unique handle of this Collection */ - @autoserialize handle: string; /** @@ -54,16 +53,18 @@ export class Collection extends DSpaceObject { /** * The Bitstream that represents the logo of this Collection */ - logo: Bitstream; + logo: RemoteData<Bitstream>; /** * An array of Collections that are direct parents of this Collection */ - parents: Array<Collection>; + parents: Array<RemoteData<Collection>>; /** * The Collection that owns this Collection */ owner: Collection; + items: Array<RemoteData<Item>>; + } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..9639abd258debc26ffad7ecbda6a7e53b9ead738 --- /dev/null +++ b/src/app/core/shared/community.model.ts @@ -0,0 +1,62 @@ +import { DSpaceObject } from "./dspace-object.model"; +import { Bitstream } from "./bitstream.model"; +import { Collection } from "./collection.model"; +import { RemoteData } from "../data/remote-data"; + +export class Community extends DSpaceObject { + + /** + * A string representing the unique handle of this Community + */ + handle: string; + + /** + * The introductory text of this Community + * Corresponds to the metadata field dc.description + */ + get introductoryText(): string { + return this.findMetadata("dc.description"); + } + + /** + * The short description: HTML + * Corresponds to the metadata field dc.description.abstract + */ + get shortDescription(): string { + return this.findMetadata("dc.description.abstract"); + } + + /** + * The copyright text of this Community + * Corresponds to the metadata field dc.rights + */ + get copyrightText(): string { + return this.findMetadata("dc.rights"); + } + + /** + * The sidebar text of this Community + * Corresponds to the metadata field dc.description.tableofcontents + */ + get sidebarText(): string { + return this.findMetadata("dc.description.tableofcontents"); + } + + /** + * The Bitstream that represents the logo of this Community + */ + logo: RemoteData<Bitstream>; + + /** + * An array of Communities that are direct parents of this Community + */ + parents: Array<RemoteData<DSpaceObject>>; + + /** + * The Community that owns this Community + */ + owner: Community; + + collections: Array<RemoteData<Collection>>; + +} diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 395886655f3090a9e17d8678954fd34a45629424..22769763bfa013a171793d733c1f2e55d883ccd4 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -2,73 +2,94 @@ import { autoserialize, autoserializeAs } from "cerialize"; import { Metadatum } from "./metadatum.model" import { isEmpty, isNotEmpty } from "../../shared/empty.util"; import { CacheableObject } from "../cache/object-cache.reducer"; +import { RemoteData } from "../data/remote-data"; /** * An abstract model class for a DSpaceObject. */ export abstract class DSpaceObject implements CacheableObject { - /** - * The human-readable identifier of this DSpaceObject - */ - @autoserialize - id: string; + @autoserialize + self: string; - /** - * The universally unique identifier of this DSpaceObject - */ - @autoserialize - uuid: string; + /** + * The human-readable identifier of this DSpaceObject + */ + @autoserialize + id: string; - /** - * A string representing the kind of DSpaceObject, e.g. community, item, … - */ - type: string; + /** + * The universally unique identifier of this DSpaceObject + */ + @autoserialize + uuid: string; - /** - * The name for this DSpaceObject - */ - @autoserialize - name: string; + /** + * A string representing the kind of DSpaceObject, e.g. community, item, … + */ + type: string; - /** - * An array containing all metadata of this DSpaceObject - */ - @autoserializeAs(Metadatum) - metadata: Array<Metadatum>; + /** + * The name for this DSpaceObject + */ + @autoserialize + name: string; - /** - * An array of DSpaceObjects that are direct parents of this DSpaceObject - */ - parents: Array<DSpaceObject>; + /** + * An array containing all metadata of this DSpaceObject + */ + @autoserializeAs(Metadatum) + metadata: Array<Metadatum>; - /** - * The DSpaceObject that owns this DSpaceObject - */ - owner: DSpaceObject; + /** + * An array of DSpaceObjects that are direct parents of this DSpaceObject + */ + parents: Array<RemoteData<DSpaceObject>>; - /** - * Find a metadata field by key and language - * - * This method returns the value of the first element - * in the metadata array that matches the provided - * key and language - * - * @param key - * @param language - * @return string - */ - findMetadata(key: string, language?: string): string { - const metadatum = this.metadata - .find((metadatum: Metadatum) => { - return metadatum.key === key && - (isEmpty(language) || metadatum.language === language) - }); - if (isNotEmpty(metadatum)) { - return metadatum.value; + /** + * The DSpaceObject that owns this DSpaceObject + */ + owner: DSpaceObject; + + /** + * Find a metadata field by key and language + * + * This method returns the value of the first element + * in the metadata array that matches the provided + * key and language + * + * @param key + * @param language + * @return string + */ + findMetadata(key: string, language?: string): string { + const metadatum = this.metadata + .find((metadatum: Metadatum) => { + return metadatum.key === key && + (isEmpty(language) || metadatum.language === language) + }); + if (isNotEmpty(metadatum)) { + return metadatum.value; + } + else { + return undefined; + } } - else { - return undefined; + + /** + * Find metadata by an array of keys + * + * This method returns the values of the element + * in the metadata array that match the provided + * key(s) + * + * @param key(s) + * @return Array<Metadatum> + */ + filterMetadata(keys: string[]): Array<Metadatum> { + return this.metadata + .filter((metadatum: Metadatum) => { + return keys.some(key => key === metadatum.key); + }); } - } } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 478d94f814e0b688bbb1a5cb81863083bf9badc6..92a05263a4f10d650bb878fceee6e78ceb3fbe0a 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -1,39 +1,80 @@ -import { inheritSerialization, autoserialize } from "cerialize"; import { DSpaceObject } from "./dspace-object.model"; import { Collection } from "./collection.model"; +import { RemoteData } from "../data/remote-data"; +import { Bundle } from "./bundle.model"; +import { Bitstream } from "./bitstream.model"; +import { Observable } from "rxjs"; -@inheritSerialization(DSpaceObject) export class Item extends DSpaceObject { - /** - * A string representing the unique handle of this Item - */ - @autoserialize - handle: string; - - /** - * The Date of the last modification of this Item - */ - lastModified: Date; - - /** - * A boolean representing if this Item is currently archived or not - */ - isArchived: boolean; - - /** - * A boolean representing if this Item is currently withdrawn or not - */ - isWithdrawn: boolean; - - /** - * An array of Collections that are direct parents of this Item - */ - parents: Array<Collection>; - - /** - * The Collection that owns this Item - */ - owner: Collection; + /** + * A string representing the unique handle of this Item + */ + handle: string; + + /** + * The Date of the last modification of this Item + */ + lastModified: Date; + + /** + * A boolean representing if this Item is currently archived or not + */ + isArchived: boolean; + + /** + * A boolean representing if this Item is currently withdrawn or not + */ + isWithdrawn: boolean; + + /** + * An array of Collections that are direct parents of this Item + */ + parents: Array<RemoteData<Collection>>; + + /** + * The Collection that owns this Item + */ + owner: Collection; + + bundles: Array<RemoteData<Bundle>>; + + getThumbnail(): Observable<Bitstream> { + const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL"); + return bundle.flatMap( + bundle => { + if (bundle != null) { + return bundle.primaryBitstream.payload; + } + else { + return Observable.of(undefined); + } + } + ); + } + + getFiles(): Observable<Array<Observable<Bitstream>>> { + const bundle: Observable <Bundle> = this.getBundle("ORIGINAL"); + return bundle.map(bundle => { + if (bundle != null) { + return bundle.bitstreams.map(bitstream => bitstream.payload) + } + }); + } + + getBundle(name: String): Observable<Bundle> { + return Observable.combineLatest( + ...this.bundles.map(b => b.payload), + (...bundles: Array<Bundle>) => bundles) + .map(bundles => { + return bundles.find((bundle: Bundle) => { + return bundle.name === name + }); + }); + } + + getCollections(): Array<Observable<Collection>> { + return this.parents.map(collection => collection.payload.map(parent => parent)); + } } diff --git a/src/app/core/shared/param-hash.spec.ts b/src/app/core/shared/param-hash.spec.ts deleted file mode 100644 index f532c1523517fae8523c239fd9a3a9b570c18051..0000000000000000000000000000000000000000 --- a/src/app/core/shared/param-hash.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ParamHash } from "./param-hash"; -describe("ParamHash", () => { - - it("should return a hash for a set of parameters", () => { - const hash = new ParamHash('azerty', true, 23).toString(); - - expect(hash).not.toBeNull(); - expect(hash).not.toBe(''); - }); - - it("should work with both simple and complex objects as parameters", () => { - const hash = new ParamHash('azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }).toString(); - - expect(hash).not.toBeNull(); - expect(hash).not.toBe(''); - }); - - it("should work with null or undefined as parameters", () => { - const hash1 = new ParamHash(undefined).toString(); - const hash2 = new ParamHash(null).toString(); - const hash3 = new ParamHash(undefined, null).toString(); - - expect(hash1).not.toBeNull(); - expect(hash1).not.toBe(''); - expect(hash2).not.toBeNull(); - expect(hash2).not.toBe(''); - expect(hash3).not.toBeNull(); - expect(hash3).not.toBe(''); - expect(hash1).not.toEqual(hash2); - expect(hash1).not.toEqual(hash3); - expect(hash2).not.toEqual(hash3); - }); - - it("should work if created without parameters", () => { - const hash1 = new ParamHash().toString(); - const hash2 = new ParamHash().toString(); - - expect(hash1).not.toBeNull(); - expect(hash1).not.toBe(''); - expect(hash1).toEqual(hash2); - }); - - it("should create the same hash if created with the same set of parameters in the same order", () => { - const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }]; - const hash1 = new ParamHash(...params).toString(); - const hash2 = new ParamHash(...params).toString(); - - expect(hash1).toEqual(hash2); - }); - - it("should create a different hash if created with the same set of parameters in a different order", () => { - const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }]; - const hash1 = new ParamHash(...params).toString(); - const hash2 = new ParamHash(...params.reverse()).toString(); - - expect(hash1).not.toEqual(hash2); - }); -}); diff --git a/src/app/core/shared/param-hash.ts b/src/app/core/shared/param-hash.ts deleted file mode 100644 index 9d07819ce512fa5f285561f57323a592375f2df6..0000000000000000000000000000000000000000 --- a/src/app/core/shared/param-hash.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Md5 } from "ts-md5/dist/md5"; - -/** - * Creates a hash of a set of parameters - */ -export class ParamHash { - private params: Array<any>; - - constructor(...params) { - this.params = params; - } - - /** - * Returns an md5 hash based on the - * params passed to the constructor - * - * If you hash the same set of params in the - * same order the hashes will be identical - * - * @return {string} - * an md5 hash - */ - toString(): string { - let hash = new Md5(); - this.params.forEach((param) => { - if (param === Object(param)) { - hash.appendStr(JSON.stringify(param)); - } - else { - hash.appendStr('' + param); - } - }); - return hash.end().toString(); - } -} diff --git a/src/app/core/url-combiner/ui-url-combiner.ts b/src/app/core/url-combiner/ui-url-combiner.ts index 260d33d1ca046341ed9a1fb66e2c1474b344bce8..c5254fdd41e52646c98fc710ca3704191110d7da 100644 --- a/src/app/core/url-combiner/ui-url-combiner.ts +++ b/src/app/core/url-combiner/ui-url-combiner.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from "../../../config"; * TODO write tests once GlobalConfig becomes injectable */ export class UIURLCombiner extends URLCombiner{ - constructor(...parts:Array<string>) { - super(GlobalConfig.ui.baseURL, GlobalConfig.ui.nameSpace, ...parts); + constructor(EnvConfig: GlobalConfig, ...parts: Array<string>) { + super(EnvConfig.ui.baseUrl, EnvConfig.ui.nameSpace, ...parts); } } diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts index 642111d87dd6def27e0eb9fd386da503326ff6de..0ed67763963c54527eb7f4b1af58bf53e77843cf 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -5,7 +5,7 @@ import { Store, StoreModule } from "@ngrx/store"; import { HeaderState } from "./header.reducer"; import Spy = jasmine.Spy; import { HeaderToggleAction } from "./header.actions"; -import { TranslateModule } from "ng2-translate"; +import { TranslateModule } from "@ngx-translate/core"; import { NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap"; import { Observable } from "rxjs"; diff --git a/src/app/home/home-news/home-news.component.html b/src/app/home/home-news/home-news.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4393228e94a22d295b254a409ccd298d1021c58c --- /dev/null +++ b/src/app/home/home-news/home-news.component.html @@ -0,0 +1,19 @@ +<!--.row to offset the app component's .container-fluid padding--> +<div class="row"> + <div class="jumbotron jumbotron-fluid"> + <div class="container-fluid"> + <h1 class="display-3">Welcome to DSpace</h1> + <p class="lead">DSpace is an open source software platform that enables organisations to:</p> + <ul> + <li>capture and describe digital material using a submission workflow module, or a variety + of + programmatic ingest options + </li> + <li>distribute an organisation's digital assets over the web through a search and retrieval + system + </li> + <li>preserve digital assets over the long term</li> + </ul> + </div> + </div> +</div> diff --git a/src/app/home/home-news/home-news.component.scss b/src/app/home/home-news/home-news.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/home/home-news/home-news.component.ts b/src/app/home/home-news/home-news.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2cf3c9cf5660a6035dcc428514e236dfbc5ce1c9 --- /dev/null +++ b/src/app/home/home-news/home-news.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'ds-home-news', + styleUrls: ['./home-news.component.css'], + templateUrl: './home-news.component.html' +}) +export class HomeNewsComponent implements OnInit { + constructor() { + this.universalInit(); + } + + universalInit() { + + } + + ngOnInit(): void { + } +} diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 06cac4108b8576fbc58329d172f9f88dfd78f8ea..fd7d4b6309832c6f3693369d0377892b7b52ceb6 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,3 +1,2 @@ -<div class="home"> - Home component -</div> +<ds-home-news></ds-home-news> +<ds-top-level-community-list></ds-top-level-community-list> diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 9cb0704a0a4c9c13b7803e20c46fa61d46f72e28..9c46b797915236e557f0026a4c0615388d1b6a9d 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,16 +1,11 @@ -import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; @Component({ - changeDetection: ChangeDetectionStrategy.Default, - encapsulation: ViewEncapsulation.Emulated, selector: 'ds-home', styleUrls: ['./home.component.css'], templateUrl: './home.component.html' }) -export class HomeComponent { - - data: any = {}; - +export class HomeComponent implements OnInit { constructor() { this.universalInit(); } @@ -19,4 +14,6 @@ export class HomeComponent { } + ngOnInit(): void { + } } diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index 5fb6a55b8db8a9cfe34e0d2511a04b12f5718986..cbb785c1c4a25a86d422516c24fecee912b48f20 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -2,13 +2,23 @@ import { NgModule } from '@angular/core'; import { HomeComponent } from './home.component'; import { HomeRoutingModule } from './home-routing.module'; +import { CommonModule } from "@angular/common"; +import { TopLevelCommunityListComponent } from "./top-level-community-list/top-level-community-list.component"; +import { HomeNewsComponent } from "./home-news/home-news.component"; +import { RouterModule } from "@angular/router"; +import { TranslateModule } from "@ngx-translate/core"; @NgModule({ imports: [ - HomeRoutingModule + CommonModule, + HomeRoutingModule, + RouterModule, + TranslateModule ], declarations: [ - HomeComponent + HomeComponent, + TopLevelCommunityListComponent, + HomeNewsComponent ] }) export class HomeModule { } diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.html b/src/app/home/top-level-community-list/top-level-community-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..7fe291ba8758a91e3a9fc36b9ff7d2c51d31d0a5 --- /dev/null +++ b/src/app/home/top-level-community-list/top-level-community-list.component.html @@ -0,0 +1,12 @@ +<div *ngIf="topLevelCommunities.hasSucceeded | async"> + <h2>{{'home.top-level-communities.head' | translate}}</h2> + <p class="lead">{{'home.top-level-communities.help' | translate}}</p> + <ul> + <li *ngFor="let community of (topLevelCommunities.payload | async)"> + <p> + <span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br> + <span class="text-muted">{{community.shortDescription}}</span> + </p> + </li> + </ul> +</div> diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.scss b/src/app/home/top-level-community-list/top-level-community-list.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.ts b/src/app/home/top-level-community-list/top-level-community-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..87f9fad5179621a46f3b4b746ffd761e3d038b3d --- /dev/null +++ b/src/app/home/top-level-community-list/top-level-community-list.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { CommunityDataService } from "../../core/data/community-data.service"; +import { RemoteData } from "../../core/data/remote-data"; +import { Community } from "../../core/shared/community.model"; + +@Component({ + selector: 'ds-top-level-community-list', + styleUrls: ['./top-level-community-list.component.css'], + templateUrl: './top-level-community-list.component.html' +}) +export class TopLevelCommunityListComponent implements OnInit { + topLevelCommunities: RemoteData<Community[]>; + + constructor( + private cds: CommunityDataService + ) { + this.universalInit(); + } + + universalInit() { + + } + + ngOnInit(): void { + this.topLevelCommunities = this.cds.findAll(); + } +} diff --git a/src/app/item-page/collections/collections.component.html b/src/app/item-page/collections/collections.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bf764ae182e1ab85852a40f623f9644394e46cf5 --- /dev/null +++ b/src/app/item-page/collections/collections.component.html @@ -0,0 +1,7 @@ +<ds-metadata-field-wrapper [label]="label | translate"> + <div class="collections"> + <a *ngFor="let collection of collections; let last=last;" [href]="(collection | async)?.self"> + <span>{{(collection | async)?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span> + </a> + </div> +</ds-metadata-field-wrapper> diff --git a/src/app/item-page/collections/collections.component.ts b/src/app/item-page/collections/collections.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..742ac247a13306204af98717edd5adf7072f4eb7 --- /dev/null +++ b/src/app/item-page/collections/collections.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Collection } from "../../core/shared/collection.model"; +import { Observable } from "rxjs"; +import { Item } from "../../core/shared/item.model"; + +@Component({ + selector: 'ds-item-page-collections', + templateUrl: './collections.component.html' +}) +export class CollectionsComponent implements OnInit { + + @Input() item: Item; + + label : string = "item.page.collections"; + + separator: string = "<br/>" + + collections: Array<Observable<Collection>>; + + constructor() { + this.universalInit(); + + } + + universalInit() { + } + + ngOnInit(): void { + this.collections = this.item.getCollections(); + } + + + +} diff --git a/src/app/item-page/file-section/file-section.component.html b/src/app/item-page/file-section/file-section.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0cc15e090ec90a9f0ddb734ca01e0ba22052acf5 --- /dev/null +++ b/src/app/item-page/file-section/file-section.component.html @@ -0,0 +1,9 @@ +<ds-metadata-field-wrapper [label]="label | translate"> + <div class="file-section"> + <a *ngFor="let file of (files | async); let last=last;" [href]="(file | async)?.retrieve"> + <span>{{(file | async)?.name}}</span> + <span>({{((file | async)?.size) | dsFileSize }})</span> + <span *ngIf="!last" innerHTML="{{separator}}"></span> + </a> + </div> +</ds-metadata-field-wrapper> diff --git a/src/app/item-page/file-section/file-section.component.ts b/src/app/item-page/file-section/file-section.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..08b01b8ea1ce124d056739abf3b7ae77df135261 --- /dev/null +++ b/src/app/item-page/file-section/file-section.component.ts @@ -0,0 +1,33 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Bitstream } from "../../core/shared/bitstream.model"; +import { Item } from "../../core/shared/item.model"; +import { Observable } from "rxjs"; + +@Component({ + selector: 'ds-item-page-file-section', + templateUrl: './file-section.component.html' +}) +export class FileSectionComponent implements OnInit { + + @Input() item: Item; + + label : string = "item.page.files"; + + separator: string = "<br/>" + + files: Observable<Array<Observable<Bitstream>>>; + + constructor() { + this.universalInit(); + + } + + universalInit() { + } + + ngOnInit(): void { + this.files = this.item.getFiles(); + } + + +} diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..64c0a607c55d153b646dfc95c28fb6cf0399388d --- /dev/null +++ b/src/app/item-page/item-page-routing.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ItemPageComponent } from './item-page.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'items/:id', pathMatch: 'full', component: ItemPageComponent }, + ]) + ] +}) +export class ItemPageRoutingModule { +} diff --git a/src/app/item-page/item-page.component.html b/src/app/item-page/item-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..47e5330ca59af793ead4dc8ec837697fbbbaedc0 --- /dev/null +++ b/src/app/item-page/item-page.component.html @@ -0,0 +1,19 @@ +<div class="item-page" *ngIf="item.hasSucceeded | async"> + <ds-item-page-title-field [item]="item.payload | async"></ds-item-page-title-field> + <div class="row"> + <div class="col-xs-12 col-md-4"> + <ds-metadata-field-wrapper> + <ds-thumbnail [thumbnail]="thumbnail | async"></ds-thumbnail> + </ds-metadata-field-wrapper> + <ds-item-page-file-section [item]="item.payload | async"></ds-item-page-file-section> + <ds-item-page-date-field [item]="item.payload | async"></ds-item-page-date-field> + <ds-item-page-author-field [item]="item.payload | async"></ds-item-page-author-field> + </div> + <div class="col-xs-12 col-md-6"> + <ds-item-page-abstract-field + [item]="item.payload | async"></ds-item-page-abstract-field> + <ds-item-page-uri-field [item]="item.payload | async"></ds-item-page-uri-field> + <ds-item-page-collections [item]="item.payload | async"></ds-item-page-collections> + </div> + </div> +</div> diff --git a/src/app/item-page/item-page.component.scss b/src/app/item-page/item-page.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..da97dd7a62e610066229abae8f4c43a981b82780 --- /dev/null +++ b/src/app/item-page/item-page.component.scss @@ -0,0 +1 @@ +@import '../../styles/variables.scss'; diff --git a/src/app/item-page/item-page.component.ts b/src/app/item-page/item-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0dcafb3b04a142dde38a321d8e91f3f9aa5f1d4 --- /dev/null +++ b/src/app/item-page/item-page.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Item } from "../core/shared/item.model"; +import { ItemDataService } from "../core/data/item-data.service"; +import { RemoteData } from "../core/data/remote-data"; +import { Observable } from "rxjs"; +import { Bitstream } from "../core/shared/bitstream.model"; + +@Component({ + selector: 'ds-item-page', + styleUrls: ['./item-page.component.css'], + templateUrl: './item-page.component.html', +}) +export class ItemPageComponent implements OnInit { + + id: number; + + private sub: any; + + item: RemoteData<Item>; + + thumbnail: Observable<Bitstream>; + + constructor(private route: ActivatedRoute, private items: ItemDataService) { + this.universalInit(); + } + + universalInit() { + + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.item = this.items.findById(params['id']); + this.thumbnail = this.item.payload.flatMap(i => i.getThumbnail()); + }); + } + + +} diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab0e2809f6467887570d262eb01ade2814e0ca52 --- /dev/null +++ b/src/app/item-page/item-page.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ItemPageComponent } from './item-page.component'; +import { ItemPageRoutingModule } from './item-page-routing.module'; +import { MetadataValuesComponent } from './metadata-values/metadata-values.component'; +import { MetadataUriValuesComponent } from './metadata-uri-values/metadata-uri-values.component'; +import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component'; +import { ItemPageAuthorFieldComponent } from './specific-field/author/item-page-author-field.component'; +import { ItemPageDateFieldComponent } from './specific-field/date/item-page-date-field.component'; +import { ItemPageAbstractFieldComponent } from './specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageUriFieldComponent } from './specific-field/uri/item-page-uri-field.component'; +import { ItemPageTitleFieldComponent } from './specific-field/title/item-page-title-field.component'; +import { ItemPageSpecificFieldComponent } from './specific-field/item-page-specific-field.component'; +import { SharedModule } from './../shared/shared.module'; +import { FileSectionComponent } from "./file-section/file-section.component"; +import { CollectionsComponent } from "./collections/collections.component"; + +@NgModule({ + declarations: [ + ItemPageComponent, + MetadataValuesComponent, + MetadataUriValuesComponent, + MetadataFieldWrapperComponent, + ItemPageAuthorFieldComponent, + ItemPageDateFieldComponent, + ItemPageAbstractFieldComponent, + ItemPageUriFieldComponent, + ItemPageTitleFieldComponent, + ItemPageSpecificFieldComponent, + FileSectionComponent, + CollectionsComponent + ], + imports: [ + ItemPageRoutingModule, + CommonModule, + SharedModule + ] +}) +export class ItemPageModule { +} diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html new file mode 100644 index 0000000000000000000000000000000000000000..638501c8573e1357c836a4a37bd1c4cec1e7b760 --- /dev/null +++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html @@ -0,0 +1,6 @@ +<div class="simple-view-element"> + <h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5> + <div class="simple-view-element-body"> + <ng-content></ng-content> + </div> +</div> \ No newline at end of file diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..749382bc9ac347c67ab1b4190df627cc954f3270 --- /dev/null +++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss @@ -0,0 +1,6 @@ +@import '../../../styles/variables.scss'; +:host { + .simple-view-element { + margin-bottom: 15px; + } +} \ No newline at end of file diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..caae4bd5f18f29e691ca41649aa430d529f357ee --- /dev/null +++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ds-metadata-field-wrapper', + styleUrls: ['./metadata-field-wrapper.component.css'], + templateUrl: './metadata-field-wrapper.component.html' +}) +export class MetadataFieldWrapperComponent { + + @Input() label: string; + + constructor() { + this.universalInit(); + + } + + universalInit() { + + } + +} diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html new file mode 100644 index 0000000000000000000000000000000000000000..cc618bcd501089c40dc33fb1a449cf5f751f0a84 --- /dev/null +++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html @@ -0,0 +1,5 @@ +<ds-metadata-field-wrapper [label]="label | translate"> + <a *ngFor="let metadatum of values; let last=last;" [href]="metadatum.value"> + {{ linktext || metadatum.value }}<span *ngIf="!last" [innerHTML]="separator"></span> + </a> +</ds-metadata-field-wrapper> diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..50be6f5ad03dee5a13636a9571c7a2e4bf85181d --- /dev/null +++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa4a9ebfc571db56c2884ff94909acd25404f84e --- /dev/null +++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { MetadataValuesComponent } from "../metadata-values/metadata-values.component"; + +@Component({ + selector: 'ds-metadata-uri-values', + styleUrls: ['./metadata-uri-values.component.css'], + templateUrl: './metadata-uri-values.component.html' +}) +export class MetadataUriValuesComponent extends MetadataValuesComponent { + + @Input() linktext: any; + + @Input() values: any; + + @Input() separator: string; + + @Input() label: string; +} diff --git a/src/app/item-page/metadata-values/metadata-values.component.html b/src/app/item-page/metadata-values/metadata-values.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f16655c63c7dfb7a10031a1288f6c01318bd79c5 --- /dev/null +++ b/src/app/item-page/metadata-values/metadata-values.component.html @@ -0,0 +1,5 @@ +<ds-metadata-field-wrapper [label]="label | translate"> + <span *ngFor="let metadatum of values; let last=last;"> + {{metadatum.value}}<span *ngIf="!last" [innerHTML]="separator"></span> + </span> +</ds-metadata-field-wrapper> diff --git a/src/app/item-page/metadata-values/metadata-values.component.scss b/src/app/item-page/metadata-values/metadata-values.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..50be6f5ad03dee5a13636a9571c7a2e4bf85181d --- /dev/null +++ b/src/app/item-page/metadata-values/metadata-values.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; diff --git a/src/app/item-page/metadata-values/metadata-values.component.ts b/src/app/item-page/metadata-values/metadata-values.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ac07ad914a385ce0030454dbd7e19ae33dee449 --- /dev/null +++ b/src/app/item-page/metadata-values/metadata-values.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ds-metadata-values', + styleUrls: ['./metadata-values.component.css'], + templateUrl: './metadata-values.component.html' +}) +export class MetadataValuesComponent { + + @Input() values: any; + + @Input() separator: string; + + @Input() label: string; + + constructor() { + this.universalInit(); + + } + + universalInit() { + + } + +} diff --git a/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e88117654363af9c7cf5dd33d15184bcef051db --- /dev/null +++ b/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../../core/shared/item.model"; +import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; + +@Component({ + selector: 'ds-item-page-abstract-field', + templateUrl: './../item-page-specific-field.component.html' +}) +export class ItemPageAbstractFieldComponent extends ItemPageSpecificFieldComponent { + + @Input() item: Item; + + separator : string; + + fields : string[] = [ + "dc.description.abstract" + ]; + + label : string = "item.page.abstract"; + +} diff --git a/src/app/item-page/specific-field/author/item-page-author-field.component.ts b/src/app/item-page/specific-field/author/item-page-author-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ff76d509aea1b7b9f130e2faca2c5ca128fbede --- /dev/null +++ b/src/app/item-page/specific-field/author/item-page-author-field.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../../core/shared/item.model"; +import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; + +@Component({ + selector: 'ds-item-page-author-field', + templateUrl: './../item-page-specific-field.component.html' +}) +export class ItemPageAuthorFieldComponent extends ItemPageSpecificFieldComponent { + + @Input() item: Item; + + separator : string; + + fields : string[] = [ + "dc.contributor.author", + "dc.creator", + "dc.contributor" + ]; + + label : string = "item.page.author"; + +} diff --git a/src/app/item-page/specific-field/date/item-page-date-field.component.ts b/src/app/item-page/specific-field/date/item-page-date-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..16b6aad1f0e47d7354f569d1ccbe2e506e053704 --- /dev/null +++ b/src/app/item-page/specific-field/date/item-page-date-field.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../../core/shared/item.model"; +import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; + +@Component({ + selector: 'ds-item-page-date-field', + templateUrl: './../item-page-specific-field.component.html' +}) +export class ItemPageDateFieldComponent extends ItemPageSpecificFieldComponent { + + @Input() item: Item; + + separator : string = ", "; + + fields : string[] = [ + "dc.date.issued" + ]; + + label : string = "item.page.date"; + +} diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.html b/src/app/item-page/specific-field/item-page-specific-field.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4a27848ec66eef61597d822207762e4e15c611da --- /dev/null +++ b/src/app/item-page/specific-field/item-page-specific-field.component.html @@ -0,0 +1,3 @@ +<div class="item-page-specific-field"> + <ds-metadata-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values> +</div> diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.ts b/src/app/item-page/specific-field/item-page-specific-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4161505ccc838a4148858f82837d2b116b162fba --- /dev/null +++ b/src/app/item-page/specific-field/item-page-specific-field.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../core/shared/item.model"; + +@Component({ + templateUrl: './item-page-specific-field.component.html' +}) +export class ItemPageSpecificFieldComponent { + + @Input() item: Item; + + fields : string[]; + + label : string; + + separator : string = "<br/>"; + + constructor() { + this.universalInit(); + } + + universalInit() { + + } +} diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.html b/src/app/item-page/specific-field/title/item-page-title-field.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4c53e2e3e21d6955da0cd478d2f0cd1b2568355e --- /dev/null +++ b/src/app/item-page/specific-field/title/item-page-title-field.component.html @@ -0,0 +1,3 @@ +<h2 class="item-page-title-field"> + <ds-metadata-values [values]="item?.filterMetadata(fields)"></ds-metadata-values> +</h2> diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.ts b/src/app/item-page/specific-field/title/item-page-title-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6dc2d159c8d5bd97d348b68ffe3f29bc2eedb062 --- /dev/null +++ b/src/app/item-page/specific-field/title/item-page-title-field.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../../core/shared/item.model"; +import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; + +@Component({ + selector: 'ds-item-page-title-field', + templateUrl: './item-page-title-field.component.html' +}) +export class ItemPageTitleFieldComponent extends ItemPageSpecificFieldComponent { + + @Input() item: Item; + + separator : string; + + fields : string[] = [ + "dc.title" + ]; + +} diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.html b/src/app/item-page/specific-field/uri/item-page-uri-field.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fde79d6a04a79e4e163026985582ba4514c1d85b --- /dev/null +++ b/src/app/item-page/specific-field/uri/item-page-uri-field.component.html @@ -0,0 +1,3 @@ +<div class="item-page-specific-field"> + <ds-metadata-uri-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values> +</div> diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts b/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f7b0bb87449f6b3dab133b6a8703d5d88ed2b69 --- /dev/null +++ b/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Item } from "../../../core/shared/item.model"; +import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; + +@Component({ + selector: 'ds-item-page-uri-field', + templateUrl: './item-page-uri-field.component.html' +}) +export class ItemPageUriFieldComponent extends ItemPageSpecificFieldComponent { + + @Input() item: Item; + + separator : string; + + fields : string[] = [ + "dc.identifier.uri" + ]; + + label : string = "item.page.uri"; + +} diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index 0daca6a146043e4cd54fe62d4b08668b340a166f..93accef6e9383f3ca507e3d8d915cf91d1eb95c9 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -24,7 +24,7 @@ import { Ng2PaginationModule } from 'ng2-pagination'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { PaginationComponent } from './pagination.component'; -import { PaginationOptions } from '../../core/shared/pagination-options.model'; +import { PaginationOptions } from '../../core/cache/models/pagination-options.model'; import { MockTranslateLoader } from "../testing/mock-translate-loader"; import { GLOBAL_CONFIG, EnvConfig } from '../../../config'; diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 20ff784a919070723a3b37004c44c970194bed6f..7705ff75dfb1896414e8e35e9bb525d450a48d90 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -15,7 +15,7 @@ import { Store } from "@ngrx/store"; import { Observable } from "rxjs"; import { HostWindowState } from "../host-window.reducer"; -import { PaginationOptions } from '../../core/shared/pagination-options.model'; +import { PaginationOptions } from '../../core/cache/models/pagination-options.model'; /** * The default pagination controls component. diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 91d590c86a1461368672a33e3362b3fd6a11d146..76f2c57ef923ab876ffd1292d2853a8cae1258a0 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -5,10 +5,13 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Ng2PaginationModule } from 'ng2-pagination'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateModule } from 'ng2-translate/ng2-translate'; +import { TranslateModule } from '@ngx-translate/core'; import { ApiService } from './api.service'; import { PaginationComponent } from "./pagination/pagination.component"; +import { FileSizePipe } from "./utils/file-size-pipe"; +import { ThumbnailComponent } from "../thumbnail/thumbnail.component"; +import { SafeUrlPipe } from "./utils/safe-url-pipe"; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -22,10 +25,13 @@ const MODULES = [ ]; const PIPES = [ + FileSizePipe, + SafeUrlPipe // put pipes here ]; const COMPONENTS = [ + ThumbnailComponent // put shared components here PaginationComponent ]; diff --git a/src/app/shared/testing/mock-translate-loader.ts b/src/app/shared/testing/mock-translate-loader.ts index a780766b259115fc9d0f8ba7c4059260e8f226b3..e739dcead3c0dbe0c2a8d4f5eddf0d7f244fbebe 100644 --- a/src/app/shared/testing/mock-translate-loader.ts +++ b/src/app/shared/testing/mock-translate-loader.ts @@ -1,4 +1,4 @@ -import { TranslateLoader } from "ng2-translate"; +import { TranslateLoader } from "@ngx-translate/core"; import { Observable } from "rxjs"; export class MockTranslateLoader implements TranslateLoader { diff --git a/src/app/shared/utils/file-size-pipe.ts b/src/app/shared/utils/file-size-pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbd3ae081cc538ca185b0a245770b33d32d846e4 --- /dev/null +++ b/src/app/shared/utils/file-size-pipe.ts @@ -0,0 +1,36 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/* + * Convert bytes into largest possible unit. + * Takes an precision argument that defaults to 2. + * Usage: + * bytes | fileSize:precision + * Example: + * {{ 1024 | fileSize}} + * formats to: 1 KB + */ +@Pipe({name: 'dsFileSize'}) +export class FileSizePipe implements PipeTransform { + + private units = [ + 'bytes', + 'KiB', + 'MiB', + 'GiB', + 'TiB', + 'PiB' + ]; + + transform(bytes: number = 0, precision: number = 2 ) : string { + if ( isNaN( parseFloat( String(bytes) )) || ! isFinite( bytes ) ) return '?'; + + let unit = 0; + + while ( bytes >= 1024 ) { + bytes /= 1024; + unit ++; + } + + return bytes.toFixed( + precision ) + ' ' + this.units[ unit ]; + } +} \ No newline at end of file diff --git a/src/app/shared/utils/safe-url-pipe.ts b/src/app/shared/utils/safe-url-pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e05e58764be52c15df5782648b3ae3429e357c0c --- /dev/null +++ b/src/app/shared/utils/safe-url-pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Pipe({name: 'dsSafeUrl'}) +export class SafeUrlPipe implements PipeTransform { + constructor(private domSanitizer: DomSanitizer) {} + transform(url) { + return this.domSanitizer.bypassSecurityTrustResourceUrl(url); + } +} \ No newline at end of file diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..757c1b8e4e983b2f4bcd815efbd3bedcc057026e --- /dev/null +++ b/src/app/thumbnail/thumbnail.component.html @@ -0,0 +1,4 @@ +<div class="thumbnail"> + <img *ngIf="thumbnail" [src]="thumbnail.retrieve"/> + <img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/> +</div> \ No newline at end of file diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..b14c7376e38df58fd2d59d62c11c2ecf6701ebe4 --- /dev/null +++ b/src/app/thumbnail/thumbnail.component.scss @@ -0,0 +1 @@ +@import '../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8e226c004982be6a8ce191beeb22c259d7e98c6 --- /dev/null +++ b/src/app/thumbnail/thumbnail.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Bitstream } from "../core/shared/bitstream.model"; + +@Component({ + selector: 'ds-thumbnail', + styleUrls: ['./thumbnail.component.css'], + templateUrl: './thumbnail.component.html' +}) +export class ThumbnailComponent { + + @Input() thumbnail: Bitstream; + + data: any = {}; + + /** + * The default 'holder.js' image + */ + holderSource: string = "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E"; + + constructor() { + this.universalInit(); + } + + universalInit() { + + } + +} diff --git a/src/backend/api.ts b/src/backend/api.ts index bc62629737ed630de7771ea1c59172439b21fd8b..2fa94e9c286d165c90ce3f4d1ebdb887e3f0d9a8 100644 --- a/src/backend/api.ts +++ b/src/backend/api.ts @@ -1,3 +1,4 @@ +import { COMMUNITIES } from "./communities"; const util = require('util'); const { Router } = require('express'); @@ -51,6 +52,67 @@ export function createMockApi() { let router = Router(); + router.route('/communities') + .get(function(req, res) { + console.log('GET'); + // 70ms latency + setTimeout(function() { + res.json(toHALResponse(req, COMMUNITIES)); + }, 0); + + // }) + // .post(function(req, res) { + // console.log('POST', util.inspect(req.body, { colors: true })); + // let community = req.body; + // if (community) { + // COMMUNITIES.push({ + // value: community.value, + // created_at: new Date(), + // completed: community.completed, + // id: COMMUNITY_COUNT++ + // }); + // return res.json(community); + // } + // + // return res.end(); + }); + + router.param('community_id', function(req, res, next, community_id) { + // ensure correct prop type + let id = req.params.community_id; + try { + req.community_id = id; + req.community = COMMUNITIES.find((community) => { + return community.id === id; + }); + next(); + } catch (e) { + next(new Error('failed to load community')); + } + }); + + router.route('/communities/:community_id') + .get(function(req, res) { + // console.log('GET', util.inspect(req.community.id, { colors: true })); + res.json(toHALResponse(req, req.community)); + // }) + // .put(function(req, res) { + // console.log('PUT', util.inspect(req.body, { colors: true })); + // + // let index = COMMUNITIES.indexOf(req.community); + // let community = COMMUNITIES[index] = req.body; + // + // res.json(community); + // }) + // .delete(function(req, res) { + // console.log('DELETE', req.community_id); + // + // let index = COMMUNITIES.indexOf(req.community); + // COMMUNITIES.splice(index, 1); + // + // res.json(req.community); + }); + router.route('/collections') .get(function(req, res) { console.log('GET'); diff --git a/src/backend/bitstreams.ts b/src/backend/bitstreams.ts index 480a0b4b55025ee7a4da054929e99ed5ee9d2595..ed47d0d94ad837e7acf59295f32785156a27ffb9 100644 --- a/src/backend/bitstreams.ts +++ b/src/backend/bitstreams.ts @@ -1,7 +1,7 @@ export const BITSTREAMS = [ { "_links": { - "self": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" }, + "self": { "href": "/bitstreams/3678" }, "bundle": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" }, "retrieve": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa/retrieve" } }, @@ -22,7 +22,7 @@ export const BITSTREAMS = [ }, { "_links": { - "self": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" }, + "self": { "href": "/bitstreams/8842" }, "bundle": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" }, "retrieve": { "href": "/rest/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632/retrieve" } }, diff --git a/src/backend/bundles.ts b/src/backend/bundles.ts index 01e8f070027b753223b62ce1d79545d226dbec7c..9ec0630dbdebfc02f4b08cc4bd15e2dd71611b3b 100644 --- a/src/backend/bundles.ts +++ b/src/backend/bundles.ts @@ -1,14 +1,14 @@ export const BUNDLES = [ { "_links": { - "self": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" }, + "self": { "href": "/bundles/2355" }, "items": [ { "href": "/items/8871" } ], "bitstreams": [ - { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" }, + { "href": "/bitstreams/3678" }, ], - "primaryBitstream": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" } + "primaryBitstream": { "href": "/bitstreams/3678" } }, "id": "2355", "uuid": "35e0606d-5e18-4f9c-aa61-74fc751cc3f9", @@ -19,14 +19,14 @@ export const BUNDLES = [ }, { "_links": { - "self": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" }, + "self": { "href": "/bundles/5687" }, "items": [ { "href": "/items/8871" } ], "bitstreams": [ - { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" }, + { "href": "/bitstreams/8842" }, ], - "primaryBitstream": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" } + "primaryBitstream": { "href": "/bitstreams/8842" } }, "id": "5687", "uuid": "a469c57a-abcf-45c3-83e4-b187ebd708fd", diff --git a/src/backend/communities.ts b/src/backend/communities.ts new file mode 100644 index 0000000000000000000000000000000000000000..940f6c72d57b64e40462770190331dc46fbf76f4 --- /dev/null +++ b/src/backend/communities.ts @@ -0,0 +1,86 @@ +export const COMMUNITIES = [ + { + "name": "Community 1", + "handle": "10673/1", + "id": "6631", + "uuid": "83cd3281-f241-48be-9234-d876f8010d14", + "type": "community", + "metadata": [ + { + "key": "dc.description", + "value": "<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\"http://www.duraspace.org/\">DuraSpace</a> logo).</p>", + "language": null + }, + { + "key": "dc.description.abstract", + "value": "This is a sample top-level community", + "language": null + }, + { + "key": "dc.description.tableofcontents", + "value": "<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>", + "language": null + }, + { + "key": "dc.rights", + "value": "<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>", + "language": null + }, + { + "key": "dc.title", + "value": "Sample Community", + "language": null + } + ], + "_links": { + "self": { + "href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863" + }, + "collections": [ + { "href": "/collections/5179" } + ] + } + }, + { + "name": "Community 2", + "handle": "10673/2", + "id": "2365", + "uuid": "80eec4c6-70bd-4beb-b3d4-5d46c6343157", + "type": "community", + "metadata": [ + { + "key": "dc.description", + "value": "<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\"http://www.duraspace.org/\">DuraSpace</a> logo).</p>", + "language": null + }, + { + "key": "dc.description.abstract", + "value": "This is a sample top-level community", + "language": null + }, + { + "key": "dc.description.tableofcontents", + "value": "<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>", + "language": null + }, + { + "key": "dc.rights", + "value": "<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>", + "language": null + }, + { + "key": "dc.title", + "value": "Sample Community", + "language": null + } + ], + "_links": { + "self": { + "href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863" + }, + "collections": [ + { "href": "/collections/6547" } + ] + } + } +]; diff --git a/src/backend/items.ts b/src/backend/items.ts index 290e2b96aa2683bd9dec9df8783f06c86c1e71ea..dc155ff98c384d041514d8a196b8b4a05d587b83 100644 --- a/src/backend/items.ts +++ b/src/backend/items.ts @@ -4,7 +4,7 @@ export const ITEMS = [ "self": { "href": "/items/8871" }, - "collections": [ + "parents": [ { "href": "/collections/5179" }, @@ -14,11 +14,11 @@ export const ITEMS = [ ], "bundles": [ { - "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" + "href": "/bundles/2355" }, - { - "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" - } + // { + // "href": "/bundles/5687" + // } ] }, "id": "8871", @@ -96,7 +96,7 @@ export const ITEMS = [ "self": { "href": "/items/9978" }, - "collections": [ + "parents": [ { "href": "/collections/5179" }, @@ -106,11 +106,11 @@ export const ITEMS = [ ], "bundles": [ { - "href": "/bundles/b0176baa-d52e-4c20-a8e6-d586f2c70c76" + "href": "/bundles/2355" }, - { - "href": "/bundles/40b1cd3f-07ad-4ca6-9716-132671f93a15" - } + // { + // "href": "/bundles/5687" + // } ] }, "id": "9978", diff --git a/src/platform/modules/browser.module.ts b/src/platform/modules/browser.module.ts index b3d809e852378deff77a13fd863462e997668d16..7f4081788f553f172add7fb3dfde5a69bc2a1a17 100755 --- a/src/platform/modules/browser.module.ts +++ b/src/platform/modules/browser.module.ts @@ -6,7 +6,8 @@ import { UniversalModule, isBrowser, isNode } from 'angular2-universal/browser'; import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { AppModule, AppComponent } from '../../app/app.module'; import { SharedModule } from '../../app/shared/shared.module'; @@ -27,8 +28,9 @@ import { GLOBAL_CONFIG, GlobalConfig, EnvConfig } from '../../config'; // import * as LRU from 'modern-lru'; -export function createTranslateLoader(http: Http) { - return new TranslateStaticLoader(http, './assets/i18n', '.json'); +// AoT requires an exported function for factories +export function HttpLoaderFactory(http: Http) { + return new TranslateHttpLoader(http); } export function getLRU(lru?: any) { @@ -51,9 +53,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [Http] + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + } }), NgbModule.forRoot(), diff --git a/src/platform/modules/node.module.ts b/src/platform/modules/node.module.ts index 7148855d4f3ce4d48e48b71f0e8886f31ad4993b..492e117521e2289b6584b3c139c154ee4f98be91 100755 --- a/src/platform/modules/node.module.ts +++ b/src/platform/modules/node.module.ts @@ -5,7 +5,8 @@ import { RouterModule } from '@angular/router'; import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node'; // for AoT we need to manually split universal packages import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; import { AppModule, AppComponent } from '../../app/app.module'; import { SharedModule } from '../../app/shared/shared.module'; @@ -13,7 +14,6 @@ import { CoreModule } from "../../app/core/core.module"; import { StoreModule, Store } from "@ngrx/store"; import { RouterStoreModule } from "@ngrx/router-store"; -import { StoreDevtoolsModule } from "@ngrx/store-devtools"; import { rootReducer, AppState, NGRX_CACHE_KEY } from '../../app/app.reducers'; import { effects } from '../../app/app.effects'; @@ -23,8 +23,9 @@ import { Meta } from '../angular2-meta'; import { GLOBAL_CONFIG, EnvConfig } from '../../config'; -export function createTranslateLoader(http: Http) { - return new TranslateStaticLoader(http, './assets/i18n', '.json'); +// AoT requires an exported function for factories +export function HttpLoaderFactory(http: Http) { + return new TranslateHttpLoader(http); } export function getLRU() { @@ -43,9 +44,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; bootstrap: [AppComponent], imports: [ TranslateModule.forRoot({ - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [Http] + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [Http] + } }), NgbModule.forRoot(), @@ -59,7 +62,6 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; AppModule, StoreModule.provideStore(rootReducer), RouterStoreModule.connectRouter(), - StoreDevtoolsModule.instrumentOnlyWithExtension(), effects ], providers: [ diff --git a/src/server.aot.ts b/src/server.aot.ts index de41c8cae8f2de03c2af27b04fc2be118734fb10..4d41fb973866775e0d3cf1a2281147c50cc9cc2c 100644 --- a/src/server.aot.ts +++ b/src/server.aot.ts @@ -124,5 +124,5 @@ app.get('*', function(req, res) { // Server let server = app.listen(app.get('port'), app.get('address'), () => { - console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`); + console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`); }); diff --git a/src/server.routes.ts b/src/server.routes.ts index 02e79e563ac2d2506f0b9fb8ac3d2219e56cb469..704ea15df53456283474ad084db5862ab797eddf 100644 --- a/src/server.routes.ts +++ b/src/server.routes.ts @@ -10,5 +10,5 @@ * ]; **/ export const routes: string[] = [ - 'home', '**' + 'home', 'items/:id' , '**' ]; diff --git a/src/server.ts b/src/server.ts index 9423add7a276e0ae20e901a313198c9a7928b48c..13837821d0c42be11c89a6f8e401ed2bbb2e9388 100644 --- a/src/server.ts +++ b/src/server.ts @@ -118,5 +118,5 @@ app.get('*', function(req, res) { // Server let server = app.listen(app.get('port'), app.get('address'), () => { - console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`); + console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`); }); diff --git a/src/typings.d.ts b/src/typings.d.ts index 2e0285e9b451cb6ec2d3fe3257ce89ca6f2c89b1..be2597c60049907fde477c4a42d9b2ecbe3e7378 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -78,3 +78,5 @@ declare module "*.json" { const value: any; export default value; } + +declare module "reflect-metadata"; diff --git a/webpack.config.ts b/webpack.config.ts index c51f1d2ce16f82f85b6b8da4363ada4e10cb392d..4da3f45b15f2fcb622023c90a5e47098cf1267d1 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -112,7 +112,7 @@ export var serverConfig = { ], }, externals: includeClientPackages( - /@angularclass|@angular|angular2-|ng2-|ng-|@ng-|angular-|@ngrx|ngrx-|@angular2|ionic|@ionic|-angular2|-ng2|-ng/ + /@angularclass|@angular|angular2-|ng2-|ng-|@ng-|angular-|@ngrx|ngrx-|@ngx-|@angular2|ionic|@ionic|-angular2|-ng2|-ng/ ), node: { global: true, diff --git a/yarn.lock b/yarn.lock index eac42106d855aace5b3615d392b83c2c7f191fdd..97fa8f8cf64820c43dea67db2b531061c020249d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,6 +113,14 @@ magic-string "^0.16.0" source-map "^0.5.6" +"@ngx-translate/core@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-6.0.1.tgz#7c7a80077feb994fc815b67a72065af04d394efe" + +"@ngx-translate/http-loader@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-0.0.3.tgz#8346c8d2d6f630254601029668f17abe2afe8a9b" + "@types/body-parser@0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-0.0.33.tgz#33ca1498fc37e51c5df0c81cae34569e7041e025" @@ -3656,10 +3664,6 @@ nested-error-stacks@^1.0.0: dependencies: inherits "~2.0.1" -ng2-translate@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ng2-translate/-/ng2-translate-4.2.0.tgz#83bc8feca329b5fc56a636e36073241c6280c659" - ngrx-store-freeze@^0.1.9: version "0.1.9" resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.1.9.tgz#b20f18f21fd5efc4e1b1e05f6f279674d0f70c81" @@ -4717,9 +4721,9 @@ reflect-metadata@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.2.tgz#ea23e5823dc830f292822bd3da9b89fd57bffb03" -reflect-metadata@0.1.8, reflect-metadata@^0.1.2: - version "0.1.8" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.8.tgz#72426d570b60776e3688968bd5ab9537a15cecf6" +reflect-metadata@^0.1.10, reflect-metadata@^0.1.2: + version "0.1.10" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.10.tgz#b4f83704416acad89988c9b15635d47e03b9344a" regenerate@^1.2.1: version "1.3.2"