diff --git a/README.md b/README.md
index de97be31ce..d979af0d4c 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 ee7b101f96..90ea2026e3 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 164c524620..d8d2acf120 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 316b12f0b6..8dd1110d1a 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 80a68206ea..95358dc446 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 a83530c27d..a227b80ab0 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -4,25 +4,6 @@
-
{{ 'example.with.data' | translate:data }}
-
{{ example }}
-
- development
- production
-
-
- {{options.id}}
-
-
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 2a58ae0aa2..7b86523886 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 d56be1a807..9b4aa66bf6 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
by CSS element selector
- de = fixture.debugElement.query(By.css('p'));
+ // query for the
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 856f964283..1cf97e763c 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 920195a0f5..42304c865e 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 0000000000..00cb50663a
--- /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
) {
+ 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 = 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 0000000000..f37f0ce4f9
--- /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,
+ ) {
+ }
+
+ buildSingle(
+ href: string,
+ normalizedType: GenericConstructor
+ ): RemoteData {
+ const requestObs = this.store.select('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) => ( entry.response).errorMessage)
+ .distinctUntilChanged();
+
+ const payload =
+ Observable.race(
+ this.objectCache.getBySelfLink(href, normalizedType),
+ responseCacheObs
+ .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
+ .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs)
+ .flatMap((resourceUUIDs: Array) => {
+ if (isNotEmpty(resourceUUIDs)) {
+ return this.objectCache.get(resourceUUIDs[0], normalizedType);
+ }
+ else {
+ return Observable.of(undefined);
+ }
+ })
+ .distinctUntilChanged()
+ ).map((normalized: TNormalized) => {
+ return this.build(normalized);
+ });
+
+ return new RemoteData(
+ href,
+ requestPending,
+ responsePending,
+ isSuccessFul,
+ errorMessage,
+ payload
+ );
+ }
+
+ buildList(
+ href: string,
+ normalizedType: GenericConstructor
+ ): RemoteData {
+ const requestObs = this.store.select('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) => ( entry.response).errorMessage)
+ .distinctUntilChanged();
+
+ const payload = responseCacheObs
+ .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
+ .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs)
+ .flatMap((resourceUUIDs: Array) => {
+ return this.objectCache.getList(resourceUUIDs, normalizedType)
+ .map((normList: TNormalized[]) => {
+ return normList.map((normalized: TNormalized) => {
+ return this.build(normalized);
+ });
+ });
+ })
+ .distinctUntilChanged();
+
+ return new RemoteData(
+ href,
+ requestPending,
+ responsePending,
+ isSuccessFul,
+ errorMessage,
+ payload
+ );
+ }
+
+
+ build(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 2edd1e8ebf..b5cd5c7b41 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 0000000000..57b4f63346
--- /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;
+
+ /**
+ * 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 0000000000..bb0e7b0708
--- /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;
+
+ /**
+ * The Item that owns this Bundle
+ */
+ owner: string;
+
+ @autoserialize
+ @relationship(NormalizedDSOType.NormalizedBitstream)
+ bitstreams: Array;
+}
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 0000000000..9b31f34837
--- /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;
+
+ /**
+ * The Collection that owns this Collection
+ */
+ owner: string;
+
+ @autoserialize
+ @relationship(NormalizedDSOType.NormalizedItem)
+ items: Array;
+
+}
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 0000000000..774abcc979
--- /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;
+
+ /**
+ * The Community that owns this Community
+ */
+ owner: string;
+
+ @autoserialize
+ @relationship(NormalizedDSOType.NormalizedCollection)
+ collections: Array;
+
+}
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 0000000000..052f7be3ee
--- /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 {
+ 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 0000000000..8ac9215b44
--- /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 0000000000..29688d5f9d
--- /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;
+
+ /**
+ * An array of DSpaceObjects that are direct parents of this DSpaceObject
+ */
+ @autoserialize
+ parents: Array;
+
+ /**
+ * 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 0000000000..cdd3acdb92
--- /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;
+
+ /**
+ * The Collection that owns this Item
+ */
+ owner: string;
+
+ @autoserialize
+ @relationship(NormalizedDSOType.NormalizedBundle)
+ bundles: Array;
+}
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 0000000000..5adc78062a
--- /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 23b0188216..85e1fdc2b3 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 9093093f50..ec0bea4a97 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) => Object.assign(new type(), entry.data));
}
+ getBySelfLink(href: string, type: GenericConstructor): Observable {
+ return this.store.select('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('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 78c6692d71..0000000000
--- 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,
- 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, 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 a478f0c2f5..0000000000
--- 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 0aa2e8c920..0000000000
--- 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;
- 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, action);
- }
-
- case RequestCacheActionTypes.FIND_BY_ID: {
- return findByIDRequest(state, action);
- }
-
- case RequestCacheActionTypes.SUCCESS: {
- return success(state, action);
- }
-
- case RequestCacheActionTypes.ERROR: {
- return error(state, action);
- }
-
- case RequestCacheActionTypes.REMOVE: {
- return removeFromCache(state, action);
- }
-
- case RequestCacheActionTypes.RESET_TIMESTAMPS: {
- return resetRequestCacheTimestamps(state, 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 c29addd23f..0000000000
--- 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;
-
- 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(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) => {
- 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) => {
- 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 efa7b0d426..0000000000
--- 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
- ) {}
-
- /**
- * 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
- * an observable of the RequestCacheEntry for this request
- */
- findAll(
- key: string,
- service: OpaqueToken,
- scopeID?: string,
- paginationOptions?: PaginationOptions,
- sortOptions?: SortOptions
- ): Observable {
- 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
- * an observable of the RequestCacheEntry for this request
- */
- findById(
- key: string,
- service: OpaqueToken,
- resourceID: string
- ): Observable {
- 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
- * an observable of the RequestCacheEntry with the specified key
- */
- get(key: string): Observable {
- return this.store.select('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('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 0000000000..45f78f10b7
--- /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 0000000000..741acf99a6
--- /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) {
+ 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 0000000000..b084842f7d
--- /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 0000000000..7e0fa6f5eb
--- /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, action);
+ }
+
+ case ResponseCacheActionTypes.REMOVE: {
+ return removeFromCache(state, action);
+ }
+
+ case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
+ return resetResponseCacheTimestamps(state, 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 0000000000..ec9da670a1
--- /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;
+//
+// 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(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) => {
+// 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) => {
+// 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 0000000000..17d9ed1091
--- /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
+ ) {}
+
+ add(key: string, response: Response, msToLive: number): Observable {
+ 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
+ * an observable of the ResponseCacheEntry with the specified key
+ */
+ get(key: string): Observable {
+ return this.store.select('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('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 b2d6c95ad5..ef9da245df 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 5cc690ed80..cdd474a909 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 71f25ee0b0..556866dbc4 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 9586940def..0000000000
--- 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 {
- 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 {
- 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 cc850900db..0000000000
--- 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 {
- 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 107ad7eca3..0000000000
--- 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 {
- protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string;
- protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string;
- protected abstract getSerializer(): Serializer;
-
- constructor(
- private EnvConfig: GlobalConfig,
- private actions$: Actions,
- private restApi: DSpaceRESTv2Service,
- private objectCache: ObjectCacheService,
- private dataService: DataService
- ) { }
-
- // 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) => ts.map(t => t.uuid))
- .map((ids: Array) => 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 ddbfa03eb4..0000000000
--- 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 {
- abstract serviceName: OpaqueToken;
- protected abstract objectCache: ObjectCacheService;
- protected abstract requestCache: RequestCacheService;
-
- constructor(private modelType: GenericConstructor) {
-
- }
-
- findAll(scopeID?: string): RemoteData> {
- 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) => {
- // use those IDs to fetch the actual objects from the ObjectCache
- return this.objectCache.getList(resourceUUIDs, this.modelType);
- }).distinctUntilChanged()
- );
- }
-
- findById(id: string): RemoteData {
- 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) => {
- if (isNotEmpty(resourceUUIDs)) {
- return this.objectCache.get(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 8c140c8398..0000000000
--- 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- {
- 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
- {
- 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 f3c8fd83af..0000000000
--- 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
- {
- 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 0000000000..232345d2be
--- /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 {
+ protected endpoint = '/collections';
+
+ constructor(
+ protected objectCache: ObjectCacheService,
+ protected responseCache: ResponseCacheService,
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected store: Store
+ ) {
+ 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 0000000000..6e63597407
--- /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 {
+ protected endpoint = '/communities';
+
+ constructor(
+ protected objectCache: ObjectCacheService,
+ protected responseCache: ResponseCacheService,
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected store: Store
+ ) {
+ 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 0000000000..af7d2697cc
--- /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 0000000000..809ff799b3
--- /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 {
+ protected abstract objectCache: ObjectCacheService;
+ protected abstract responseCache: ResponseCacheService;
+ protected abstract requestService: RequestService;
+ protected abstract rdbService: RemoteDataBuildService;
+ protected abstract store: Store;
+ protected abstract endpoint: string;
+
+ constructor(private normalizedResourceType: GenericConstructor) {
+
+ }
+
+ protected getFindAllHref(scopeID?): string {
+ let result = this.endpoint;
+ if (hasValue(scopeID)) {
+ result += `?scope=${scopeID}`
+ }
+ return result;
+ }
+
+ findAll(scopeID?: string): RemoteData> {
+ 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(href, this.normalizedResourceType);
+ // return this.rdbService.buildList(href);
+ }
+
+ protected getFindByIDHref(resourceID): string {
+ return `${this.endpoint}/${resourceID}`;
+ }
+
+ findById(id: string): RemoteData {
+ 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(href, this.normalizedResourceType);
+ // return this.rdbService.buildSingle(href);
+ }
+
+ findByHref(href: string): RemoteData {
+ 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(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 0000000000..fc13999f37
--- /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 {
+ protected endpoint = '/items';
+
+ constructor(
+ protected objectCache: ObjectCacheService,
+ protected responseCache: ResponseCacheService,
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected store: Store
+) {
+ 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 26f13ea1b5..af5a0658a3 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
+ 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 1b9ff177ef..7fa02bf25c 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 {
-
constructor(
- private storeLoading: Observable,
+ public self: string,
+ private requestPending: Observable,
+ private responsePending: Observable,
+ private isSuccessFul: Observable,
public errorMessage: Observable,
public payload: Observable
) {
@@ -23,13 +23,17 @@ export class RemoteData {
get state(): Observable {
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 b8dde51159..3c650d95f1 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
) { }
/**
@@ -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 0000000000..16ce1963bd
--- /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;
+
+ constructor(
+ request: Request
+ ) {
+ 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 0000000000..e5d887626e
--- /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
+ ) { }
+
+ @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): Array => 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 => [co.uuid])
+
+ ).map((ids: Array) => 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 0000000000..9171bbe509
--- /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 {
+ constructor(
+ public href: string,
+ public resourceType: GenericConstructor
+ ) {}
+}
+
+export class FindByIDRequest extends Request {
+ constructor(
+ href: string,
+ resourceType: GenericConstructor,
+ public resourceID: string
+ ) {
+ super(href, resourceType);
+ }
+}
+
+export class FindAllRequest extends Request {
+ constructor(
+ href: string,
+ resourceType: GenericConstructor,
+ 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 0000000000..e20accc831
--- /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;
+ 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, action);
+ }
+
+ case RequestActionTypes.EXECUTE: {
+ return executeRequest(state, action);
+ }
+
+ case RequestActionTypes.COMPLETE: {
+ return completeRequest(state, 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 0000000000..b3b28af2c2
--- /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
+ ) {
+ }
+
+ isPending(href: string): boolean {
+ let isPending = false;
+ this.store.select('core', 'data', 'request', href)
+ .take(1)
+ .subscribe((re: RequestEntry) => {
+ isPending = (hasValue(re) && !re.completed)
+ });
+
+ return isPending;
+ }
+
+ get(href: string): Observable {
+ return this.store.select('core', 'data', 'request', href);
+ }
+
+ configure(href: string, normalizedType: GenericConstructor): 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 2661b3708d..236244873c 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 b5fa5983d8..d4d5a7ce59 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 implements Serializer {
if (Array.isArray(response._embedded)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
- return Deserialize(response._embedded, this.modelType);
+ let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._embedded._links));
+ return Deserialize(normalized, this.modelType);
}
/**
@@ -70,7 +71,26 @@ export class DSpaceRESTv2Serializer implements Serializer {
if (!Array.isArray(response._embedded)) {
throw new Error('Expected an Array, use deserialize() instead');
}
- return > Deserialize(response._embedded, this.modelType);
+ let normalized = response._embedded.map((resource) => {
+ return Object.assign({}, resource, this.normalizeLinks(resource._links));
+ });
+
+ return > 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 1a4b26510b..6015104003 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 0000000000..8c00f2d96c
--- /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 0000000000..2e1c8ae8d1
--- /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 0000000000..8cb46566df
--- /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, action);
+ }
+
+ case HrefIndexActionTypes.REMOVE_UUID: {
+ return removeUUIDFromHrefIndex(state, 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 0000000000..e7e3d7218a
--- /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 43217a1292..5325e395d8 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;
+ parents: Array>;
/**
* 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 b990c8617e..7c2f6b05d4 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;
/**
* An array of Items that are direct parents of this Bundle
*/
- parents: Array
- ;
+ parents: Array>;
/**
* The Item that owns this Bundle
*/
owner: Item;
+ bitstreams: Array>
+
}
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts
index 7048ded4a4..4287eff63c 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;
/**
* An array of Collections that are direct parents of this Collection
*/
- parents: Array;
+ parents: Array>;
/**
* The Collection that owns this Collection
*/
owner: Collection;
+ items: Array>;
+
}
diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts
new file mode 100644
index 0000000000..9639abd258
--- /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;
+
+ /**
+ * An array of Communities that are direct parents of this Community
+ */
+ parents: Array>;
+
+ /**
+ * The Community that owns this Community
+ */
+ owner: Community;
+
+ collections: Array>;
+
+}
diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts
index 395886655f..22769763bf 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;
+ /**
+ * The name for this DSpaceObject
+ */
+ @autoserialize
+ name: string;
- /**
- * An array of DSpaceObjects that are direct parents of this DSpaceObject
- */
- parents: Array;
+ /**
+ * An array containing all metadata of this DSpaceObject
+ */
+ @autoserializeAs(Metadatum)
+ metadata: Array;
- /**
- * The DSpaceObject that owns this DSpaceObject
- */
- owner: DSpaceObject;
+ /**
+ * An array of DSpaceObjects that are direct parents of this DSpaceObject
+ */
+ parents: Array>;
- /**
- * 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
+ */
+ filterMetadata(keys: string[]): Array {
+ 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 478d94f814..92a05263a4 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;
+ /**
+ * A string representing the unique handle of this Item
+ */
+ handle: string;
- /**
- * The Date of the last modification of this Item
- */
- lastModified: Date;
+ /**
+ * 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 archived or not
+ */
+ isArchived: boolean;
- /**
- * A boolean representing if this Item is currently withdrawn or not
- */
- isWithdrawn: 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;
+ /**
+ * An array of Collections that are direct parents of this Item
+ */
+ parents: Array>;
- /**
- * The Collection that owns this Item
- */
- owner: Collection;
+ /**
+ * The Collection that owns this Item
+ */
+ owner: Collection;
+
+ bundles: Array>;
+
+ getThumbnail(): Observable {
+ const bundle: Observable = this.getBundle("THUMBNAIL");
+ return bundle.flatMap(
+ bundle => {
+ if (bundle != null) {
+ return bundle.primaryBitstream.payload;
+ }
+ else {
+ return Observable.of(undefined);
+ }
+ }
+ );
+ }
+
+ getFiles(): Observable>> {
+ const bundle: Observable = this.getBundle("ORIGINAL");
+ return bundle.map(bundle => {
+ if (bundle != null) {
+ return bundle.bitstreams.map(bitstream => bitstream.payload)
+ }
+ });
+ }
+
+ getBundle(name: String): Observable {
+ return Observable.combineLatest(
+ ...this.bundles.map(b => b.payload),
+ (...bundles: Array) => bundles)
+ .map(bundles => {
+ return bundles.find((bundle: Bundle) => {
+ return bundle.name === name
+ });
+ });
+ }
+
+ getCollections(): Array> {
+ 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 f532c15235..0000000000
--- 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 9d07819ce5..0000000000
--- 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;
-
- 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 260d33d1ca..c5254fdd41 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) {
- super(GlobalConfig.ui.baseURL, GlobalConfig.ui.nameSpace, ...parts);
+ constructor(EnvConfig: GlobalConfig, ...parts: Array) {
+ 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 642111d87d..0ed6776396 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 0000000000..4393228e94
--- /dev/null
+++ b/src/app/home/home-news/home-news.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
Welcome to DSpace
+
DSpace is an open source software platform that enables organisations to:
+
+ - capture and describe digital material using a submission workflow module, or a variety
+ of
+ programmatic ingest options
+
+ - distribute an organisation's digital assets over the web through a search and retrieval
+ system
+
+ - preserve digital assets over the long term
+
+
+
+
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 0000000000..e69de29bb2
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 0000000000..2cf3c9cf56
--- /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 06cac4108b..fd7d4b6309 100644
--- a/src/app/home/home.component.html
+++ b/src/app/home/home.component.html
@@ -1,3 +1,2 @@
-
- Home component
-
+
+
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts
index 9cb0704a0a..9c46b79791 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 5fb6a55b8d..cbb785c1c4 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 0000000000..7fe291ba87
--- /dev/null
+++ b/src/app/home/top-level-community-list/top-level-community-list.component.html
@@ -0,0 +1,12 @@
+
+
{{'home.top-level-communities.head' | translate}}
+
{{'home.top-level-communities.help' | translate}}
+
+
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 0000000000..e69de29bb2
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 0000000000..87f9fad517
--- /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;
+
+ 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 0000000000..bf764ae182
--- /dev/null
+++ b/src/app/item-page/collections/collections.component.html
@@ -0,0 +1,7 @@
+
+
+
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 0000000000..742ac247a1
--- /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 = "
"
+
+ collections: Array>;
+
+ 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 0000000000..0cc15e090e
--- /dev/null
+++ b/src/app/item-page/file-section/file-section.component.html
@@ -0,0 +1,9 @@
+
+
+
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 0000000000..08b01b8ea1
--- /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 = "
"
+
+ files: Observable>>;
+
+ 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 0000000000..64c0a607c5
--- /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 0000000000..47e5330ca5
--- /dev/null
+++ b/src/app/item-page/item-page.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 0000000000..da97dd7a62
--- /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 0000000000..c0dcafb3b0
--- /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- ;
+
+ thumbnail: Observable;
+
+ 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 0000000000..ab0e2809f6
--- /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 0000000000..638501c857
--- /dev/null
+++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html
@@ -0,0 +1,6 @@
+
\ 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 0000000000..749382bc9a
--- /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 0000000000..caae4bd5f1
--- /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 0000000000..cc618bcd50
--- /dev/null
+++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html
@@ -0,0 +1,5 @@
+
+
+ {{ linktext || metadatum.value }}
+
+
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 0000000000..50be6f5ad0
--- /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 0000000000..fa4a9ebfc5
--- /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 0000000000..f16655c63c
--- /dev/null
+++ b/src/app/item-page/metadata-values/metadata-values.component.html
@@ -0,0 +1,5 @@
+
+
+ {{metadatum.value}}
+
+
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 0000000000..50be6f5ad0
--- /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 0000000000..6ac07ad914
--- /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 0000000000..3e88117654
--- /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 0000000000..2ff76d509a
--- /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 0000000000..16b6aad1f0
--- /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 0000000000..4a27848ec6
--- /dev/null
+++ b/src/app/item-page/specific-field/item-page-specific-field.component.html
@@ -0,0 +1,3 @@
+
+
+
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 0000000000..4161505ccc
--- /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 = "
";
+
+ 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 0000000000..4c53e2e3e2
--- /dev/null
+++ b/src/app/item-page/specific-field/title/item-page-title-field.component.html
@@ -0,0 +1,3 @@
+
+
+
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 0000000000..6dc2d159c8
--- /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 0000000000..fde79d6a04
--- /dev/null
+++ b/src/app/item-page/specific-field/uri/item-page-uri-field.component.html
@@ -0,0 +1,3 @@
+
+
+
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 0000000000..4f7b0bb874
--- /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 0daca6a146..93accef6e9 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 20ff784a91..7705ff75df 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 91d590c86a..76f2c57ef9 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 a780766b25..e739dcead3 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 0000000000..fbd3ae081c
--- /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 0000000000..e05e58764b
--- /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 0000000000..757c1b8e4e
--- /dev/null
+++ b/src/app/thumbnail/thumbnail.component.html
@@ -0,0 +1,4 @@
+
+
![]()
+
![]()
+
\ 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 0000000000..b14c7376e3
--- /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 0000000000..e8e226c004
--- /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 bc62629737..2fa94e9c28 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 480a0b4b55..ed47d0d94a 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 01e8f07002..9ec0630dbd 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 0000000000..940f6c72d5
--- /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": "This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
",
+ "language": null
+ },
+ {
+ "key": "dc.description.abstract",
+ "value": "This is a sample top-level community",
+ "language": null
+ },
+ {
+ "key": "dc.description.tableofcontents",
+ "value": "This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
",
+ "language": null
+ },
+ {
+ "key": "dc.rights",
+ "value": "If this Community had special copyright text to display, it would be displayed here.
",
+ "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": "This is the introductory text for the Sample Community on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).
\r\nDSpace Communities may contain one or more Sub-Communities or Collections (of Items).
\r\nThis particular Community has its own logo (the DuraSpace logo).
",
+ "language": null
+ },
+ {
+ "key": "dc.description.abstract",
+ "value": "This is a sample top-level community",
+ "language": null
+ },
+ {
+ "key": "dc.description.tableofcontents",
+ "value": "This is the news section for this Sample Community. System or Community Administrators (of this Community) can edit this News field.
",
+ "language": null
+ },
+ {
+ "key": "dc.rights",
+ "value": "If this Community had special copyright text to display, it would be displayed here.
",
+ "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 290e2b96aa..dc155ff98c 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 b3d809e852..7f4081788f 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 7148855d4f..492e117521 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 de41c8cae8..4d41fb9738 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 02e79e563a..704ea15df5 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 9423add7a2..13837821d0 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 2e0285e9b4..be2597c600 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 c51f1d2ce1..4da3f45b15 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 eac42106d8..97fa8f8cf6 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"