Merge branch 'master' of https://github.com/DSpace/dspace-angular into all-test

This commit is contained in:
Matteo Perelli
2017-03-13 09:29:01 +01:00
30 changed files with 1271 additions and 1288 deletions

View File

@@ -28,6 +28,10 @@ npm start
``` ```
Then go to [http://localhost:3000](http://localhost:3000) in your browser Then go to [http://localhost:3000](http://localhost:3000) in your browser
NOTE: currently there's not much to see at that URL. We really do need your help. If you're interested in jumping in, and you've made it this far, please look at the [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular), grab a card, and get to work. Thanks!
Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below.
## Table of Contents ## Table of Contents
* [Introduction to the technology](#introduction-to-the-technology) * [Introduction to the technology](#introduction-to-the-technology)
* [Requirements](#requirements) * [Requirements](#requirements)

View File

@@ -0,0 +1,19 @@
import { ProtractorPage } from './pagenotfound.po';
describe('protractor PageNotFound', function() {
let page: ProtractorPage;
beforeEach(() => {
page = new ProtractorPage();
});
it('should contain element ds-pagenotfound when navigating to page that doesnt exist"', () => {
page.navigateToNonExistingPage();
expect(page.elementTagExists("ds-pagenotfound")).toEqual(true);
});
it('should not contain element ds-pagenotfound when navigating to existing page"', () => {
page.navigateToExistingPage();
expect(page.elementTagExists("ds-pagenotfound")).toEqual(false);
});
});

View File

@@ -0,0 +1,19 @@
import { browser, element, by } from 'protractor';
export class ProtractorPage {
HOMEPAGE : string = "/home";
NONEXISTINGPAGE : string = "/e9019a69-d4f1-4773-b6a3-bd362caa46f2";
navigateToNonExistingPage() {
return browser.get(this.NONEXISTINGPAGE);
}
navigateToExistingPage() {
return browser.get(this.HOMEPAGE);
}
elementTagExists(tag : string) {
return element(by.tagName(tag)).isPresent();
}
}

1452
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,11 +36,12 @@
"ngc": "ngc -p tsconfig.aot.json", "ngc": "ngc -p tsconfig.aot.json",
"prestart": "npm run build:prod:ngc:json", "prestart": "npm run build:prod:ngc:json",
"server": "node dist/server/build.js", "server": "node dist/server/build.js",
"server:dev": "nodemon --debug dist/server/index.js", "server:dev": "node dist/server/index.js",
"server:dev:watch": "nodemon --debug dist/server/index.js",
"start": "npm run server", "start": "npm run server",
"start:dev": "npm run clean:prod && npm run build && npm run server", "start:dev": "npm run clean:prod && npm run build && npm run server:dev",
"watch": "webpack -w & npm run style:watch", "watch": "webpack -w & npm run style:watch",
"watch:dev:server": "concurrently \"npm run server:dev\" \"npm run watch\"", "watch:dev:server": "concurrently \"npm run server:dev:watch\" \"npm run watch\"",
"watch:dev": "npm run clean:prod && npm run build && npm run watch:dev:server", "watch:dev": "npm run clean:prod && npm run build && npm run watch:dev:server",
"watch:prod:server": "concurrently \"npm run server\" \"npm run watch\"", "watch:prod:server": "concurrently \"npm run server\" \"npm run watch\"",
"watch:prod": "npm run build:prod:ngc:json && npm run watch:prod:server", "watch:prod": "npm run build:prod:ngc:json && npm run watch:prod:server",
@@ -62,7 +63,7 @@
"coverage": "http-server -c-1 -o -p 9875 ./coverage", "coverage": "http-server -c-1 -o -p 9875 ./coverage",
"webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444", "webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444",
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone", "webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone",
"rewrap": "npm shrinkwrap --dev" "rewrap": "npm run clean && npm install --no-optional && npm shrinkwrap --dev"
}, },
"dependencies": { "dependencies": {
"@angular/common": "2.2.3", "@angular/common": "2.2.3",
@@ -84,6 +85,7 @@
"@ngrx/router-store": "^1.2.5", "@ngrx/router-store": "^1.2.5",
"@ngrx/store": "^2.2.1", "@ngrx/store": "^2.2.1",
"@ngrx/store-devtools": "^3.2.2", "@ngrx/store-devtools": "^3.2.2",
"@types/jsonschema": "0.0.5",
"angular2-express-engine": "2.1.0-rc.1", "angular2-express-engine": "2.1.0-rc.1",
"angular2-platform-node": "2.1.0-rc.1", "angular2-platform-node": "2.1.0-rc.1",
"angular2-universal": "2.1.0-rc.1", "angular2-universal": "2.1.0-rc.1",
@@ -91,11 +93,13 @@
"autoprefixer": "^6.5.4", "autoprefixer": "^6.5.4",
"body-parser": "1.15.2", "body-parser": "1.15.2",
"bootstrap": "4.0.0-alpha.5", "bootstrap": "4.0.0-alpha.5",
"cerialize": "^0.1.13",
"compression": "1.6.2", "compression": "1.6.2",
"express": "4.14.0", "express": "4.14.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"http-server": "^0.9.0", "http-server": "^0.9.0",
"js.clone": "0.0.3", "js.clone": "0.0.3",
"jsonschema": "^1.1.1",
"methods": "1.1.2", "methods": "1.1.2",
"morgan": "1.7.0", "morgan": "1.7.0",
"ng2-translate": "4.2.0", "ng2-translate": "4.2.0",

View File

@@ -1,13 +1,27 @@
{ {
"title": "DSpace", "example": {
"with": {
"data": "{{greeting}}, {{recipient}}!"
}
},
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "DSpace software",
"link.duraspace": "DuraSpace"
},
"nav": { "nav": {
"home": "Home" "home": "Home"
}, },
"example": { "title": "DSpace",
"with": {
"data": "{{greeting}}, {{recipient}}!" "404": {
"help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
"page-not-found": "page not found",
"link": {
"home-page": "Take me to the home page"
} }
} }
} }

View File

@@ -1,10 +1,12 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ path: '', redirectTo: '/home', pathMatch: 'full' } { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent},
]) ])
], ],
}) })

View File

@@ -1,11 +1,17 @@
<ds-header></ds-header> <div class="outer-wrapper">
<div class="inner-wrapper">
<ds-header></ds-header>
<div class="container-fluid"> <main class="main-content">
<main> <div class="container-fluid">
<p>{{ 'example.with.data' | translate:data }}</p> <p>{{ 'example.with.data' | translate:data }}</p>
<p>{{ example }}</p> <p>{{ example }}</p>
<h2 *ngIf="!env" style="color:green">development</h2> <h2 *ngIf="!env" style="color:green">development</h2>
<h2 *ngIf="env" style="color:red">production</h2> <h2 *ngIf="env" style="color:red">production</h2>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div>
</main> </main>
<ds-footer></ds-footer>
</div>
</div> </div>

View File

@@ -1 +1,18 @@
// Sticky Footer
.outer-wrapper {
display: flex;
margin: 0;
}
.inner-wrapper {
flex: 1 1 auto;
flex-flow: column nowrap;
display: flex;
min-height: 100vh;
flex-direction: column;
}
.main-content {
flex: 1 0 auto;
}

View File

@@ -1,4 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreModule } from './core/core.module';
import { HomeModule } from './home/home.module'; import { HomeModule } from './home/home.module';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
@@ -6,6 +8,7 @@ import { SharedModule } from './shared/shared.module';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component'; import { HeaderComponent } from './header/header.component';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { StoreModule } from "@ngrx/store"; import { StoreModule } from "@ngrx/store";
import { RouterStoreModule } from "@ngrx/router-store"; import { RouterStoreModule } from "@ngrx/router-store";
@@ -17,11 +20,13 @@ import { effects } from './app.effects';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
HeaderComponent HeaderComponent,
PageNotFoundComponent
], ],
imports: [ imports: [
SharedModule, SharedModule,
HomeModule, HomeModule,
CoreModule.forRoot(),
AppRoutingModule, AppRoutingModule,
/** /**
* StoreModule.provideStore is imported once in the root module, accepting a reducer * StoreModule.provideStore is imported once in the root module, accepting a reducer

View File

@@ -0,0 +1,45 @@
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";
const IMPORTS = [
CommonModule,
SharedModule
];
const DECLARATIONS = [
FooterComponent
];
const EXPORTS = [
FooterComponent
];
const PROVIDERS = [
];
@NgModule({
imports: [ ...IMPORTS ],
declarations: [...DECLARATIONS],
exports: [...EXPORTS],
providers: [...PROVIDERS]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (isNotEmpty(parentModule)) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only');
}
}
static forRoot(): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
...PROVIDERS
]
};
}
}

View File

@@ -0,0 +1,4 @@
export interface DSpaceRESTV2Response {
_embedded?: any;
_links?: any;
}

View File

@@ -0,0 +1,93 @@
{
"title": "DSpace REST v2 json-schema",
"description": "Based on http://hyperschema.org/mediatypes/hal",
"type": "object",
"properties": {
"_links": {
"$ref": "#/definitions/links"
},
"_embedded": {
"$ref": "#/definitions/embedded"
}
},
"definitions": {
"links": {
"title": "HAL Links",
"description": "Object of links with the rels as the keys",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"$ref": "#/definitions/linkObject"
},
{
"$ref": "#/definitions/linkArray"
}
]
}
},
"linkArray": {
"title": "HAL Link Array",
"description": "An array of linkObjects of the same link relation",
"type": "array",
"items": {
"$ref": "#/definitions/linkObject"
}
},
"linkObject": {
"title": "HAL Link Object",
"description": "An object with link information",
"type": "object",
"properties": {
"name": {
"$ref": "http://hyperschema.org/core/base#/definitions/name"
},
"href": {
"anyOf": [
{
"$ref": "http://hyperschema.org/core/link#/definitions/href"
},
{
"$ref": "http://hyperschema.org/core/link#/definitions/hrefTemplated"
}
]
},
"templated": {
"$ref": "http://hyperschema.org/core/link#/definitions/isTemplated"
},
"type": {
"$ref": "http://hyperschema.org/core/base#/definitions/mediaType"
},
"deprecation": {
"$ref": "http://hyperschema.org/core/link#/definitions/isDeprecated"
}
},
"required": [
"href"
]
},
"embedded": {
"title": "HAL Embedded Resource",
"description": "An embedded HAL resource",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"$ref": "#"
},
{
"$ref": "#/definitions/embeddedArray"
}
]
}
},
"embeddedArray": {
"title": "HAL Embedded Array",
"description": "An array of embedded resources with the same link relation",
"type": "array",
"items": {
"$ref": "#"
}
}
}
}

View File

@@ -0,0 +1,201 @@
import { DSpaceRESTv2Serializer } from "./dspace-rest-v2.serializer";
import { autoserialize, autoserializeAs } from "cerialize";
class TestModel {
@autoserialize
id: string;
@autoserialize
name: string;
@autoserializeAs(TestModel)
parents?: Array<TestModel>;
}
const testModels = [
{
"id": "d4466d54-d73b-4d8f-b73f-c702020baa14",
"name": "Model 1",
},
{
"id": "752a1250-949a-46ad-9bea-fbc45f0b656d",
"name": "Model 2",
}
];
const testResponses = [
{
"_links": {
"self": "/testmodels/9e32a2e2-6b91-4236-a361-995ccdc14c60",
"parents": [
{ "href": "/testmodels/21539b1d-9ef1-4eda-9c77-49565b5bfb78" },
{ "href": "/testmodels/be8325f7-243b-49f4-8a4b-df2b793ff3b5" }
]
},
"id": "9e32a2e2-6b91-4236-a361-995ccdc14c60",
"type": "testModels",
"name": "A Test Model"
},
{
"_links": {
"self": "/testmodels/598ce822-c357-46f3-ab70-63724d02d6ad",
"parents": [
{ "href": "/testmodels/be8325f7-243b-49f4-8a4b-df2b793ff3b5" },
{ "href": "/testmodels/21539b1d-9ef1-4eda-9c77-49565b5bfb78" }
]
},
"id": "598ce822-c357-46f3-ab70-63724d02d6ad",
"type": "testModels",
"name": "Another Test Model"
}
];
const parentHrefRegex = /^\/testmodels\/(.+)$/g;
describe("DSpaceRESTv2Serializer", () => {
describe("serialize", () => {
it("should turn a model in to a valid document", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = serializer.serialize(testModels[0]);
expect(testModels[0].id).toBe(doc._embedded.id);
expect(testModels[0].name).toBe(doc._embedded.name);
});
});
describe("serializeArray", () => {
it("should turn an array of models in to a valid document", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = serializer.serializeArray(testModels);
expect(testModels[0].id).toBe(doc._embedded[0].id);
expect(testModels[0].name).toBe(doc._embedded[0].name);
expect(testModels[1].id).toBe(doc._embedded[1].id);
expect(testModels[1].name).toBe(doc._embedded[1].name);
});
});
describe("deserialize", () => {
it("should turn a valid document describing a single entity in to a valid model", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = {
"_embedded": testResponses[0],
};
const model = serializer.deserialize(doc);
expect(model.id).toBe(doc._embedded.id);
expect(model.name).toBe(doc._embedded.name);
});
//TODO cant implement/test this yet - depends on how relationships
// will be handled in the rest api
// it("should retain relationship information", () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = {
// "_embedded": testResponses[0],
// };
//
// const model = serializer.deserialize(doc);
//
// console.log(model);
//
// const modelParentIds = model.parents.map(parent => parent.id).sort();
// const responseParentIds = doc._embedded._links.parents
// .map(parent => parent.href)
// .map(href => href.replace(parentHrefRegex, '$1'))
// .sort();
//
// expect(modelParentIds).toEqual(responseParentIds);
// });
// TODO enable once validation is enabled in the serializer
// it("should throw an error when dealing with an invalid document", () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = testResponses[0];
//
// expect(() => {
// serializer.deserialize(doc);
// }).toThrow();
// });
it("should throw an error when dealing with a document describing an array", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = {
"_embedded": testResponses
};
expect(() => {
serializer.deserialize(doc);
}).toThrow();
});
});
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 cant implement/test this yet - depends on how relationships
// will be handled in the rest api
// it("should retain relationship information", () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = {
// "_embedded": testResponses,
// };
//
// const models = serializer.deserializeArray(doc);
//
// models.forEach((model, i) => {
// const modelParentIds = model.parents.map(parent => parent.id).sort();
// const responseParentIds = doc._embedded[i]._links.parents
// .map(parent => parent.href)
// .map(href => href.replace(parentHrefRegex, '$1'))
// .sort();
//
// expect(modelParentIds).toEqual(responseParentIds);
// });
// });
// TODO enable once validation is enabled in the serializer
// it("should throw an error when dealing with an invalid document", () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = testResponses[0];
//
// expect(() => {
// serializer.deserializeArray(doc);
// }).toThrow();
// });
it("should throw an error when dealing with a document describing a single model", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = {
"_embedded": testResponses[0]
};
expect(() => {
serializer.deserializeArray(doc);
}).toThrow();
});
});
});

View File

@@ -0,0 +1,82 @@
import { Serialize, Deserialize } from "cerialize";
import { Serializer } from "../serializer";
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
import { DSpaceRESTv2Validator } from "./dspace-rest-v2.validator";
/**
* ensures we can use 'typeof T' as a type
* more details:
* https://github.com/Microsoft/TypeScript/issues/204#issuecomment-257722306
*/
type Constructor<T> = { new (...args: any[]): T } | ((...args: any[]) => T) | Function;
/**
* This Serializer turns responses from v2 of DSpace's REST API
* to models and vice versa
*/
export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
/**
* Create a new DSpaceRESTv2Serializer instance
*
* @param modelType a class or interface to indicate
* the kind of model this serializer should work with
*/
constructor(private modelType: Constructor<T>) {
}
/**
* Convert a model in to the format expected by the backend
*
* @param model The model to serialize
* @returns An object to send to the backend
*/
serialize(model: T): DSpaceRESTV2Response {
return {
"_embedded": Serialize(model, this.modelType)
};
}
/**
* Convert an array of models in to the format expected by the backend
*
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: Array<T>): DSpaceRESTV2Response {
return {
"_embedded": Serialize(models, this.modelType)
};
}
/**
* Convert a response from the backend in to a model.
*
* @param response An object returned by the backend
* @returns a model of type T
*/
deserialize(response: DSpaceRESTV2Response): T {
// TODO enable validation, once rest data stabilizes
// new DSpaceRESTv2Validator(response).validate();
if (Array.isArray(response._embedded)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
return <T> Deserialize(response._embedded, this.modelType);
}
/**
* Convert a response from the backend in to an array of models
*
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: DSpaceRESTV2Response): Array<T> {
//TODO enable validation, once rest data stabilizes
// new DSpaceRESTv2Validator(response).validate();
if (!Array.isArray(response._embedded)) {
throw new Error('Expected an Array, use deserialize() instead');
}
return <Array<T>> Deserialize(response._embedded, this.modelType);
}
}

View File

@@ -0,0 +1,34 @@
import * as schema from './dspace-rest-v2.schema.json'
import { Validator } from "jsonschema";
/**
* Verifies a document is a valid response from
* a DSpace REST API v2
*/
export class DSpaceRESTv2Validator {
constructor(private document: any) {
}
/**
* Throws an exception if this.document isn't a valid response from
* a DSpace REST API v2. Succeeds otherwise.
*/
validate(): void {
const validator = new Validator();
const result = validator.validate(this.document, schema);
if (!result.valid) {
if (result.errors && result.errors.length > 0) {
const message = result.errors
.map((error) => error.message)
.join("\n");
throw new Error(message);
}
else {
throw new Error("JSON API validation failed for an unknown reason");
}
}
}
}

View File

@@ -0,0 +1,9 @@
<footer class="footer">
<div class="container-fluid content-container-fluid">
<p>
<a href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
{{ 'footer.copyright' | translate:{year : dateObj | date:'y'} }}
<a href="http://www.duraspace.org/">{{ 'footer.link.duraspace' | translate}}</a>
</p>
</div>
</footer>

View File

@@ -0,0 +1,17 @@
@import '../../../styles/variables.scss';
@import '../../../../node_modules/bootstrap/scss/_variables.scss';
$footer-bg: $gray-lighter;
$footer-border: 1px solid darken($footer-bg, 10%);
$footer-padding: $spacer * 1.5;
.footer {
background-color: $footer-bg;
border-top: $footer-border;
text-align:center;
padding: $footer-padding;
p {
margin: 0;
}
}

View File

@@ -0,0 +1,60 @@
// ... test imports
import {
async,
ComponentFixture,
inject,
TestBed
} from '@angular/core/testing';
import {
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from "@angular/core";
import { By } from '@angular/platform-browser';
import { TranslateModule, TranslateLoader } from "ng2-translate";
import { Store, StoreModule } from "@ngrx/store";
// Load the implementations that should be tested
import { FooterComponent } from './footer.component';
import { CommonModule } from '@angular/common';
import { MockTranslateLoader } from "../../shared/testing/mock-translate-loader";
let comp: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
let de: DebugElement;
let el: HTMLElement;
describe('Footer component', () => {
// async beforeEach
beforeEach(async(() => {
return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
provide: TranslateLoader,
useClass: MockTranslateLoader
})],
declarations: [FooterComponent], // declare the test component
providers: [
FooterComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
comp = fixture.componentInstance; // component test instance
// query for the title <p> by CSS element selector
de = fixture.debugElement.query(By.css('p'));
el = de.nativeElement;
});
it('should create footer', inject([FooterComponent], (app: FooterComponent) => {
// Perform test using fixture and service
expect(app).toBeTruthy();
}));
});

View File

@@ -0,0 +1,18 @@
import { Component, OnInit } from "@angular/core";
@Component({
selector: 'ds-footer',
styleUrls: ['footer.component.css'],
templateUrl: 'footer.component.html'
})
export class FooterComponent implements OnInit {
dateObj: number = Date.now();
constructor() {
}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,38 @@
/**
* A Serializer turns responses from the backend to models
* and vice versa
*/
export interface Serializer<T> {
/**
* Convert a model in to the format expected by the backend
*
* @param model The model to serialize
* @returns An object to send to the backend
*/
serialize(model: T): any;
/**
* Convert an array of models in to the format expected by the backend
*
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: Array<T>): any;
/**
* Convert a response from the backend in to a model.
*
* @param response An object returned by the backend
* @returns a model of type T
*/
deserialize(response: any): T;
/**
* Convert a response from the backend in to an array of models
*
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: any): Array<T>;
}

View File

@@ -0,0 +1,10 @@
<div class="page-not-found">
<h1>404</h1>
<h2><small>{{"404.page-not-found" | translate}}</small></h2>
<br>
<p>{{"404.help" | translate}}</p>
<br>
<p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a>
</p>
</div>

View File

@@ -0,0 +1 @@
@import '../../styles/variables.scss';

View File

@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-pagenotfound',
styleUrls: ['./pagenotfound.component.css'],
templateUrl: './pagenotfound.component.html'
})
export class PageNotFoundComponent {
data: any = {};
constructor() {
this.universalInit();
}
universalInit() {
}
}

View File

@@ -0,0 +1,219 @@
import { isEmpty, hasNoValue, hasValue, isNotEmpty } from "./empty.util";
describe("Empty Utils", () => {
const string: string = 'string';
const fn: () => void = function () {};
const object: any = { length: 0 };
const emptyMap: Map<any, any> = new Map();
let fullMap: Map<string,string> = new Map();
fullMap.set('foo', 'bar');
describe("hasNoValue", () => {
it("should return true for null", () => {
expect(hasNoValue(null)).toBe(true);
});
it("should return true for undefined", () => {
expect(hasNoValue(undefined)).toBe(true);
});
it("should return false for an empty String", () => {
expect(hasNoValue('')).toBe(false);
});
it("should return false for true", () => {
expect(hasNoValue(true)).toBe(false);
});
it("should return false for false", () => {
expect(hasNoValue(false)).toBe(false);
});
it("should return false for a String", () => {
expect(hasNoValue(string)).toBe(false);
});
it("should return false for a Function", () => {
expect(hasNoValue(fn)).toBe(false);
});
it("should return false for 0", () => {
expect(hasNoValue(0)).toBe(false);
});
it("should return false for an empty Array", () => {
expect(hasNoValue([])).toBe(false);
});
it("should return false for an empty Object", () => {
expect(hasNoValue({})).toBe(false);
});
});
describe("hasValue", () => {
it("should return false for null", () => {
expect(hasValue(null)).toBe(false);
});
it("should return false for undefined", () => {
expect(hasValue(undefined)).toBe(false);
});
it("should return true for an empty String", () => {
expect(hasValue('')).toBe(true);
});
it("should return true for false", () => {
expect(hasValue(false)).toBe(true);
});
it("should return true for true", () => {
expect(hasValue(true)).toBe(true);
});
it("should return true for a String", () => {
expect(hasValue(string)).toBe(true);
});
it("should return true for a Function", () => {
expect(hasValue(fn)).toBe(true);
});
it("should return true for 0", () => {
expect(hasValue(0)).toBe(true);
});
it("should return true for an empty Array", () => {
expect(hasValue([])).toBe(true);
});
it("should return true for an empty Object", () => {
expect(hasValue({})).toBe(true);
});
});
describe("isEmpty", () => {
it("should return true for null", () => {
expect(isEmpty(null)).toBe(true);
});
it("should return true for undefined", () => {
expect(isEmpty(undefined)).toBe(true);
});
it("should return true for an empty String", () => {
expect(isEmpty('')).toBe(true);
});
it("should return false for a whitespace String", () => {
expect(isEmpty(' ')).toBe(false);
expect(isEmpty('\n\t')).toBe(false);
});
it("should return false for true", () => {
expect(isEmpty(true)).toBe(false);
});
it("should return false for false", () => {
expect(isEmpty(false)).toBe(false);
});
it("should return false for a String", () => {
expect(isEmpty(string)).toBe(false);
});
it("should return false for a Function", () => {
expect(isEmpty(fn)).toBe(false);
});
it("should return false for 0", () => {
expect(isEmpty(0)).toBe(false);
});
it("should return true for an empty Array", () => {
expect(isEmpty([])).toBe(true);
});
it("should return false for an empty Object", () => {
expect(isEmpty({})).toBe(false);
});
it("should return true for an Object that has zero \'length\'", () => {
expect(isEmpty(object)).toBe(true);
});
it("should return true for an Empty map", () => {
expect(isEmpty(emptyMap)).toBe(true);
});
it("should return false for a Map that is not empty", () => {
expect(isEmpty(fullMap)).toBe(false);
});
});
describe("isNotEmpty", () => {
it("should return false for null", () => {
expect(isNotEmpty(null)).toBe(false);
});
it("should return false for undefined", () => {
expect(isNotEmpty(undefined)).toBe(false);
});
it("should return false for an empty String", () => {
expect(isNotEmpty('')).toBe(false);
});
it("should return true for a whitespace String", () => {
expect(isNotEmpty(' ')).toBe(true);
expect(isNotEmpty('\n\t')).toBe(true);
});
it("should return true for false", () => {
expect(isNotEmpty(false)).toBe(true);
});
it("should return true for true", () => {
expect(isNotEmpty(true)).toBe(true);
});
it("should return true for a String", () => {
expect(isNotEmpty(string)).toBe(true);
});
it("should return true for a Function", () => {
expect(isNotEmpty(fn)).toBe(true);
});
it("should return true for 0", () => {
expect(isNotEmpty(0)).toBe(true);
});
it("should return false for an empty Array", () => {
expect(isNotEmpty([])).toBe(false);
});
it("should return true for an empty Object", () => {
expect(isNotEmpty({})).toBe(true);
});
it("should return false for an Object that has zero length", () => {
expect(isNotEmpty(object)).toBe(false);
});
it("should return false for an Empty map", () => {
expect(isNotEmpty(emptyMap)).toBe(false);
});
it("should return true for a Map that is not empty", () => {
expect(isNotEmpty(fullMap)).toBe(true);
});
});
});

View File

@@ -0,0 +1,94 @@
/**
Returns true if the passed value is null or undefined.
hasNoValue(); // true
hasNoValue(null); // true
hasNoValue(undefined); // true
hasNoValue(''); // false
hasNoValue([]); // false
hasNoValue(function() {}); // false
*/
export function hasNoValue(obj?: any): boolean {
return obj === null || obj === undefined;
}
/**
Returns true if the passed value is not null or undefined.
hasValue(); // false
hasValue(null); // false
hasValue(undefined); // false
hasValue(''); // true
hasValue([]); // true
hasValue(function() {}); // true
*/
export function hasValue(obj?: any): boolean {
return !hasNoValue(obj);
}
/**
Verifies that a value is `null` or an empty string, empty array,
or empty function.
isEmpty(); // true
isEmpty(null); // true
isEmpty(undefined); // true
isEmpty(''); // true
isEmpty([]); // true
isEmpty({}); // false
isEmpty('Adam Hawkins'); // false
isEmpty([0,1,2]); // false
isEmpty('\n\t'); // false
isEmpty(' '); // false
*/
export function isEmpty(obj?: any): boolean {
if (hasNoValue(obj)) {
return true;
}
if (typeof obj.size === 'number') {
return !obj.size;
}
let objectType = typeof obj;
if (objectType === 'object') {
let size = obj['size'];
if (typeof size === 'number') {
return !size;
}
}
if (typeof obj.length === 'number' && objectType !== 'function') {
return !obj.length;
}
if (objectType === 'object') {
let length = obj['length'];
if (typeof length === 'number') {
return !length;
}
}
return false;
}
/**
Verifies that a value is not `null`, an empty string, empty array,
or empty function.
isNotEmpty(); // false
isNotEmpty(null); // false
isNotEmpty(undefined); // false
isNotEmpty(''); // false
isNotEmpty([]); // false
isNotEmpty({}); // true
isNotEmpty('Adam Hawkins'); // true
isNotEmpty([0,1,2]); // true
isNotEmpty('\n\t'); // true
isNotEmpty(' '); // true
*/
export function isNotEmpty(obj?: any): boolean {
return !isEmpty(obj);
}

View File

@@ -11,6 +11,7 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra
import { AppModule, AppComponent } from './app/app.module'; import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module'; import { SharedModule } from './app/shared/shared.module';
import { CacheService } from './app/shared/cache.service'; import { CacheService } from './app/shared/cache.service';
import { CoreModule } from "./app/core/core.module";
// Will be merged into @angular/platform-browser in a later release // Will be merged into @angular/platform-browser in a later release
// see https://github.com/angular/angular/pull/12322 // see https://github.com/angular/angular/pull/12322
@@ -56,7 +57,8 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
RouterModule.forRoot([], { useHash: false, preloadingStrategy: IdlePreload }), RouterModule.forRoot([], { useHash: false, preloadingStrategy: IdlePreload }),
IdlePreloadModule.forRoot(), IdlePreloadModule.forRoot(),
SharedModule.forRoot(), CoreModule.forRoot(),
SharedModule,
AppModule, AppModule,
], ],
providers: [ providers: [

View File

@@ -10,6 +10,7 @@ import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-tra
import { AppModule, AppComponent } from './app/app.module'; import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module'; import { SharedModule } from './app/shared/shared.module';
import { CacheService } from './app/shared/cache.service'; import { CacheService } from './app/shared/cache.service';
import { CoreModule } from "./app/core/core.module";
// Will be merged into @angular/platform-browser in a later release // Will be merged into @angular/platform-browser in a later release
// see https://github.com/angular/angular/pull/12322 // see https://github.com/angular/angular/pull/12322
@@ -47,7 +48,8 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
FormsModule, FormsModule,
RouterModule.forRoot([], { useHash: false }), RouterModule.forRoot([], { useHash: false }),
SharedModule.forRoot(), CoreModule.forRoot(),
SharedModule,
AppModule, AppModule,
], ],
providers: [ providers: [

View File

@@ -10,5 +10,5 @@
* ]; * ];
**/ **/
export const routes: string[] = [ export const routes: string[] = [
'home' 'home', '**'
]; ];

7
src/typings.d.ts vendored
View File

@@ -71,3 +71,10 @@ interface WebpackRequire {
interface NodeRequire extends WebpackRequire { } interface NodeRequire extends WebpackRequire { }
interface NodeModule extends WebpackModule { } interface NodeModule extends WebpackModule { }
interface Global extends GlobalEnvironment { } interface Global extends GlobalEnvironment { }
// Allows us to import json files in typescript
// See https://hackernoon.com/import-json-into-typescript-8d465beded79#.88tfoy2df
declare module "*.json" {
const value: any;
export default value;
}