Merge pull request #123 from wwelling/angular-upgrade

Angular upgrade: this connects to #83 and this connects to #86
This commit is contained in:
Art Lowel
2017-07-20 18:03:45 +02:00
committed by GitHub
257 changed files with 7549 additions and 5887 deletions

124
README.md
View File

@@ -91,13 +91,15 @@ To change the default configuration values, create local files that override the
To use the configuration parameters in your component:
```bash
import { GlobalConfig } from "../config";
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig) {}
```
Running the app
---------------
After you have installed all dependencies you can now run the app. Run `yarn run watch:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
After you have installed all dependencies you can now run the app. Run `yarn run watch` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
Running in production mode
--------------------------
@@ -113,7 +115,7 @@ yarn start
If you only want to build for production, without starting, run:
```bash
yarn run build:prod:ngc:json
yarn run build:prod
```
This will build the application and put the result in the `dist` folder
@@ -155,7 +157,7 @@ If you are going to use a remote test enviroment you need to edit the './protrac
The default browser is Google Chrome.
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:`yarn run watch:dev`
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:`yarn run watch`
or any command that bring up the DSpace interface.
@@ -171,6 +173,17 @@ To run all the tests (e.g.: to run tests with Continuous Integration software) y
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
Deploy
------
```bash
# deploy production in standalone pm2 container
yarn run deploy
# remove production from standalone pm2 container
yarn run undeploy
```
Other commands
--------------
@@ -204,34 +217,35 @@ File Structure
```
dspace-angular
├── README.md * This document
├── app.json * Application manifest file
├── app.yaml * Application manifest file
├── config * Folder for configuration files
│   ── environment.default.js * Default configuration files
├── dist * Folder for e2e test files
├── e2e *
│   ── environment.default.js * Default configuration files
│   └── environment.test.js * Test configuration files
├── e2e * Folder for e2e test files
│   ├── app.e2e-spec.ts *
│   ├── app.po.ts *
│   ├── pagenotfound *
│   │   ├── pagenotfound.e2e-spec.ts *
│   │   └── pagenotfound.po.ts *
│   └── tsconfig.json *
├── empty.js *
├── helpers.js *
├── karma.conf.js * Unit Test configuration file
│   └── tsconfig.json * TypeScript configuration file for e2e tests
├── karma.conf.js * Karma configuration file for Unit Test
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
├── postcss.config.json * PostCSS (http://postcss.org/) configuration file
├── protractor.conf.js * E2E tests configuration file
├── postcss.config.js * PostCSS (http://postcss.org/) configuration file
├── protractor.conf.js *
├── resources * Folder for static resources
│   ├── data * Folder for static data
│   │   └── en * Folder for i18n English data
│   ├── i18n * Folder for i18n translations
│   │   └── en.json *
│   │   └── en.json * i18n translations for English
│   └── images * Folder for images
│   ── dspace_logo.png *
├── rollup-client.js * Rollup (http://rollupjs.org/) configuration for the client
├── rollup-server.js * Rollup (http://rollupjs.org/) configuration for the server
│   ── dspace-logo-old.png *
│   ├── dspace-logo.png *
│   └── favicon.ico *
├── rollup.config.js * Rollup (http://rollupjs.org/) configuration
├── spec-bundle.js *
├── src * The source of the application
│   ├── app * The location of the app module, and root of the application shared by client and server
│   ├── app *
│   │   ├── app-routing.module.ts *
│   │   ├── app.component.html *
│   │   ├── app.component.scss *
@@ -239,48 +253,62 @@ dspace-angular
│   │   ├── app.component.ts *
│   │   ├── app.effects.ts *
│   │   ├── app.module.ts *
│   │   ├── app.reducers.ts *
│   │   ├── app.reducer.ts *
│   │   ├── browser-app.module.ts * The root module for the client
│   │   ├── collection-page *
│   │   ├── community-page *
│   │   ├── core *
│   │   ├── header *
│   │   ├── home *
│   │   ├── item-page *
│   │   ├── object-list *
│   │   ├── pagenotfound *
│   │   ├── server-app.module.ts * The root module for the server
│   │   ├── shared *
│   │   ── store.actions.ts *
│   │   ── store.actions.ts *
│   │   ├── store.effects.ts *
│   │   ├── thumbnail *
│   │   └── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
│   ├── backend * Folder containing a mock of the REST API, hosted by the express server
│   │   ├── api.ts *
│   │   ├── bitstreams.ts *
│   │   ├── bundles.ts *
│   │   ├── cache.ts *
│   │   ├── collections.ts *
│   │   ── db.ts *
│   │   ├── items.ts *
│   │   ── metadata.ts *
│   ├── client.aot.ts * The bootstrap file for the client, in production
│   ├── client.ts * The bootstrap file for the client, during development
│   │   ├── data *
│   │   ── db.ts *
│   ├── config *
│   │   ── cache-config.interface.ts *
│   │   ├── global-config.interface.ts *
│   │   └── server-config.interface.ts *
│   ├── config.ts * File that loads environmental and shareable settings and makes them available to app components
│   ├── index.html * The index.html file
│   ├── platform *
│   │   ├── angular2-meta.ts *
│   │   ├── modules *
│   │   │   ├── browser.module.ts * The root module for the client
│   │   │   └── node.module.ts * The root module for the server
│   │   ── workarounds *
│   │   ├── __workaround.browser.ts *
│   │   └── __workaround.node.ts *
│   ├── server.aot.ts * The express (http://expressjs.com/) config and bootstrap file for the server, in production
│   ├── server.routes.ts * The routes file for the server
│   ├── server.ts * The express (http://expressjs.com/) config and bootstrap file for the server, during development
│   ├── styles * Folder containing global styles.
│   │   ├── main.scss * Global scss file
│   ├── main.browser.ts * The bootstrap file for the client
│   ├── main.server.aot.ts * The express (http://expressjs.com/) config and bootstrap file for the server, in production
│   ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server, during development
│   ├── modules *
│   │   ├── cookies *
│   │   ── data-loader *
│   │   ├── transfer-http *
│   │   ├── transfer-state *
│   │   ├── transfer-store *
│   │   └── translate-universal-loader.ts *
│   ├── routes.ts * The routes file for the server
│   ├── styles * Folder containing global styles
│   │   ├── _mixins.scss *
│   │   └── variables.scss * Global sass variables file
│   ── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
├── tsconfig.aot.json * TypeScript config for production builds
├── tsconfig.json * TypeScript config for development build
│   ── tsconfig.browser.json * TypeScript config for the client build
│   ├── tsconfig.server.aot.json * TypeScript config for the server build with Ahead of Time
│   ├── tsconfig.server.json * TypeScript config for the server build
│   └── tsconfig.test.json * TypeScript config for the test build
├── tsconfig.json * TypeScript config
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── typedoc.json * TYPEDOC configuration
├── webpack.config.ts * Webpack (https://webpack.github.io/) config for development builds
├── webpack.prod.config.ts * Webpack (https://webpack.github.io/) config for production builds
├── webpack.test.config.js * Webpack (https://webpack.github.io/) config for testing
├── webpack * Webpack (https://webpack.github.io/) config directory
│   ├── helpers.js *
│   ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
│   ├── webpack.common.js *
│   ├── webpack.prod.js * Webpack (https://webpack.github.io/) config for production build
│   ├── webpack.server.js * Webpack (https://webpack.github.io/) config for server build
│   └── webpack.test.js * Webpack (https://webpack.github.io/) config for test build
├── webpack.config.ts *
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
```

View File

@@ -1,12 +0,0 @@
{
"name": "Angular 2 Universal Starter",
"description": "Angular 2 Universal starter kit by @AngularClass",
"repository": "https://github.com/angular/universal-starter",
"logo": "https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png",
"env": {
"NPM_CONFIG_PRODUCTION": {
"description": "Install `webpack` and other development modules when deploying to allow full builds.",
"value": "false"
}
}
}

17
app.yaml Normal file
View File

@@ -0,0 +1,17 @@
# Copyright 2015-2016, Google, Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# [START app_yaml]
runtime: nodejs
env: flex
# [END app_yaml]

View File

@@ -1,28 +1,38 @@
module.exports = {
// Angular Universal server settings.
ui: {
ssl: false,
host: 'localhost',
port: 3000,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: '/'
},
// The REST API server settings.
"rest": {
"ssl": false,
"address": "dspace7.4science.it",
"port": 80,
rest: {
ssl: false,
host: 'dspace7.4science.it',
port: 80,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
"nameSpace": "/dspace-spring-rest/api"
nameSpace: '/dspace-spring-rest/api'
},
// Angular2 UI server settings.
"ui": {
"ssl": false,
"address": "localhost",
"port": 3000,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
"nameSpace": "/"
// Caching settings
cache: {
// NOTE: how long should objects be cached for by default
msToLive: 15 * 60 * 1000, // 15 minute
control: 'max-age=60' // revalidate browser
},
"cache": {
// how long should objects be cached for by default
"msToLive": 15 * 60 * 1000, // 15 minute
"control": "max-age=60" // revalidate browser
// Angular Universal settings
universal: {
preboot: true,
async: true,
time: false
},
"universal": {
// Angular Universal settings
"preboot": true,
"async": true
}
// Log directory
logDirectory: '.',
// NOTE: rehydrate or replay
// rehydrate will transfer prerender state to browser state, actions do not need to replay
// replay will transfer an array of actions to browser, actions replay automatically
prerenderStrategy: 'rehydrate',
// NOTE: will log all redux actions and transfers in console
debug: false
};

View File

@@ -0,0 +1,3 @@
module.exports = {
};

View File

@@ -1,6 +1,6 @@
import { ProtractorPage } from './app.po';
describe('protractor App', function() {
describe('protractor App', () => {
let page: ProtractorPage;
beforeEach(() => {
@@ -9,11 +9,11 @@ describe('protractor App', function() {
it('should display title "DSpace"', () => {
page.navigateTo();
expect(page.getPageTitleText()).toEqual('DSpace');
expect<any>(page.getPageTitleText()).toEqual('DSpace');
});
it('should display header "Welcome to DSpace"', () => {
page.navigateTo();
expect(page.getFirstHeaderText()).toEqual('Welcome to DSpace');
expect<any>(page.getFirstHeaderText()).toEqual('Welcome to DSpace');
});
});

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
module.exports = {
NgProbeToken: {},
_createConditionalRootRenderer: function(rootRenderer, extraTokens, coreTokens) {
return rootRenderer;
},
__platform_browser_private__: {}
};

View File

@@ -1,24 +0,0 @@
/**
* @author: @AngularClass
*/
var path = require('path');
// Helper functions
var ROOT = path.resolve(__dirname, '.');
function hasProcessFlag(flag) {
return process.argv.join('').indexOf(flag) > -1;
}
function isWebpackDevServer() {
return process.argv[1] && !! (/webpack-dev-server/.exec(process.argv[1]));
}
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [ROOT].concat(args));
}
exports.hasProcessFlag = hasProcessFlag;
exports.isWebpackDevServer = isWebpackDevServer;
exports.root = root;

View File

@@ -2,9 +2,11 @@
* @author: @AngularClass
*/
module.exports = function(config) {
module.exports = function (config) {
var testWebpackConfig = require('./webpack.test.config.js')({env: 'test'});
var testWebpackConfig = require('./webpack/webpack.test.js')({
env: 'test'
});
// Uncomment and change to run tests on a remote Selenium server
var webdriverConfig = {
@@ -15,7 +17,7 @@ module.exports = function(config) {
var configuration = {
// base path that will be used to resolve all patterns (e.g. files, exclude)
basePath: '.',
basePath: '',
/*
* Frameworks to use
@@ -25,15 +27,18 @@ module.exports = function(config) {
frameworks: ['jasmine'],
plugins: [
require('karma-webpack'),
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-phantomjs-launcher'),
require('karma-webdriver-launcher'),
require('karma-coverage'),
require('karma-remap-coverage'),
require('karma-mocha-reporter'),
require('karma-remap-istanbul'),
require('karma-sourcemap-loader'),
require('karma-webpack')
require("istanbul-instrumenter-loader"),
require("karma-istanbul-preprocessor")
],
// list of files to exclude
@@ -44,54 +49,60 @@ module.exports = function(config) {
*
* we are building the test environment in ./spec-bundle.js
*/
files: [
{
pattern: './spec-bundle.js',
watched: false
}
],
files: [{
pattern: './spec-bundle.js',
watched: false
}],
/*
* preprocess matching files before serving them to the browser
* available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
*/
preprocessors: {
'./spec-bundle.js': ['coverage', 'webpack', 'sourcemap']
'./spec-bundle.js': ['istanbul', 'webpack', 'sourcemap']
},
// Webpack Config at ./webpack.test.js
webpack: testWebpackConfig,
// save interim raw coverage report in memory
coverageReporter: {
reporters: [
{
type: 'in-memory'
}, {
type: 'json',
subdir: '.',
file: 'coverage-final.json'
}, {
type: 'html',
dir: 'coverage/'
}
]
type: 'in-memory'
},
remapCoverageReporter: {
'text-summary': null,
json: './coverage/coverage.json',
html: './coverage/html'
'text-summary': null, // to show summary in console
html: './coverage/html',
cobertura: './coverage/cobertura.xml'
},
remapIstanbulReporter: {
remapOptions: {}, //additional remap options
reports: {
html: 'coverage'
json: 'coverage/coverage.json',
lcovonly: 'coverage/lcov.info',
html: 'coverage/html/',
}
},
// Webpack please don't spam the console when running in karma!
/**
* Webpack please don't spam the console when running in karma!
*/
webpackMiddleware: {
stats: 'errors-only'
/**
* webpack-dev-middleware configuration
* i.e.
*/
noInfo: true,
/**
* and use stats to turn off verbose output
*/
stats: {
/**
* options i.e.
*/
chunks: false
}
},
/*
@@ -100,9 +111,7 @@ module.exports = function(config) {
* possible values: 'dots', 'progress'
* available reporters: https://npmjs.org/browse/keyword/karma-reporter
*/
reporters: [
'mocha', 'coverage', 'karma-remap-istanbul'
],
reporters: ['mocha', 'coverage', 'remap-coverage', 'karma-remap-istanbul'],
// Karma web server port
port: 9876,
@@ -114,10 +123,10 @@ module.exports = function(config) {
* level of logging
* possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
*/
logLevel: config.LOG_INFO,
logLevel: config.LOG_WARN,
// enable / disable watching file and executing tests whenever any file changes
//autoWatch: true,
autoWatch: false,
/*
* start these browsers
@@ -125,9 +134,6 @@ module.exports = function(config) {
*/
browsers: [
'Chrome'
//'ChromeTravisCi',
//'SeleniumChrome',
//'SeleniumFirefox'
],
customLaunchers: {
@@ -156,11 +162,6 @@ module.exports = function(config) {
browserNoActivityTimeout: 30000
/*
* Continuous Integration mode
* if true, Karma captures browsers, runs the tests and exits
*/
//singleRun: true
};
if (process.env.TRAVIS) {

View File

@@ -1,6 +1,7 @@
{
"watch": [
"dist",
"config",
"src/index.html"
],
"ext": "js ts json html"

View File

@@ -1,187 +1,200 @@
{
"name": "dspace-angular",
"version": "0.0.0",
"description": "Angular 2 Universal UI for DSpace",
"version": "0.0.1",
"description": "Angular Universal UI for DSpace",
"repository": {
"type": "git",
"url": "https://github.com/dspace/dspace-angular.git"
},
"license": "BSD-2-Clause",
"engines": {
"node": ">=5.0.0"
},
"scripts": {
"clean:log": "rimraf *.log*",
"clean:dist": "rimraf dist/*",
"clean:node": "rimraf node_modules",
"clean:ngc": "rimraf **/*.ngfactory.ts",
"clean:json": "rimraf *.records.json",
"clean:css": "rimraf src/**/*.css",
"clean:css:ts": "rimraf src/**/*.css.ts",
"clean:scss:ts": "rimraf src/**/*.scss.ts",
"clean:css:shim:ts": "rimraf src/**/*.css.shim.ts",
"clean:scss:shim:ts": "rimraf src/**/*.scss.shim.ts",
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
"clean:coverage": "rimraf coverage",
"clean:prod": "yarn run clean:ngc && yarn run clean:json && yarn run clean:css && yarn run clean:css:ts && yarn run clean:scss:ts && yarn run clean:css:shim:ts && yarn run clean:scss:shim:ts && yarn run clean:dist",
"clean": "yarn run clean:log && yarn run clean:prod && yarn run clean:coverage && yarn run clean:node",
"sass": "node-sass src -o src --include-path node_modules --output-style compressed -q",
"postcss": "node node_modules/postcss-cli/bin/postcss -c postcss.config.json",
"style": "yarn run sass && yarn run postcss",
"style:watch": "nodemon -e scss -w src -x \"yarn run style\"",
"rollup": "rollup -c rollup-server.js && rollup -c rollup-client.js",
"prebuild": "yarn run clean:dist && yarn run style",
"clean:dist": "rimraf dist",
"clean:doc": "rimraf doc",
"clean:log": "rimraf *.log*",
"clean:json": "rimraf *.records.json",
"clean:node": "rimraf node_modules",
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json ",
"clean": "yarn run clean:prod && yarn run clean:node",
"prebuild": "yarn run clean:dist",
"prebuild:aot": "yarn run prebuild",
"prebuild:prod": "yarn run prebuild",
"build": "webpack --progress",
"build:prod": "webpack --config webpack.prod.config.ts",
"build:prod:rollup": "yarn run build:prod && yarn run rollup",
"build:prod:ngc": "yarn run clean:prod && yarn run style && yarn run ngc && yarn run build:prod:rollup",
"build:prod:ngc:json": "yarn run clean:prod && yarn run style && yarn run ngc && yarn run build:prod:json:rollup",
"build:prod:json": "webpack --config webpack.prod.config.ts --json | webpack-bundle-size-analyzer",
"build:prod:json:rollup": "yarn run build:prod:json && yarn run rollup",
"ngc": "ngc -p tsconfig.aot.json",
"prestart": "yarn run build:prod:ngc:json",
"server": "node dist/server/index.js",
"server:dev": "node dist/server/index.js",
"server:dev:watch": "nodemon --debug dist/server/index.js",
"build:aot": "webpack --env.aot --env.server && webpack --env.aot --env.client",
"build:prod": "webpack --env.aot --env.server -p && webpack --env.aot --env.client -p",
"postbuild:prod": "yarn run rollup",
"rollup": "rollup -c rollup.config.js",
"prestart": "yarn run build:prod",
"prestart:dev": "yarn run build",
"start": "yarn run server",
"start:dev": "yarn run clean:prod && yarn run build && yarn run server:dev",
"watch": "webpack -w & yarn run style:watch",
"watch:dev:server": "npm-run-all -p server:dev:watch watch",
"watch:dev": "yarn run clean:prod && yarn run build && yarn run watch:dev:server",
"watch:prod:server": "npm-run-all -p server watch",
"watch:prod": "yarn run build:prod:ngc:json && yarn run watch:prod:server",
"start:dev": "yarn run server",
"deploy": "pm2 start dist/server.js",
"predeploy": "npm run build:prod",
"preundeploy": "pm2 stop dist/server.js",
"undeploy": "pm2 delete dist/server.js",
"postundeploy": "npm run clean:dist",
"server": "node dist/server.js",
"server:watch": "nodemon dist/server.js",
"server:watch:debug": "nodemon --debug dist/server.js",
"webpack:watch": "webpack -w",
"webpack:watch:aot": "webpack -w --env.aot --env.server && webpack -w --env.aot --env.client",
"webpack:watch:prod": "webpack -w --env.aot --env.server -p && webpack -w --env.aot --env.client -p",
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
"watch:aot": "yarn run build:aot && npm-run-all -p webpack:watch:aot server:watch",
"watch:prod": "yarn run build:prod && npm-run-all -p webpack:watch:prod server:watch",
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
"watch:debug:aot": "yarn run build:aot && npm-run-all -p webpack:watch:aot server:watch:debug",
"watch:debug:prod": "yarn run build:prod && npm-run-all -p webpack:watch:prod server:watch:debug",
"predebug": "yarn run build",
"debug": "node --debug-brk dist/server/index.js",
"debug:server": "node-nightly --inspect --debug-brk dist/server/index.js",
"debug:start": "yarn run build && yarn run debug:server",
"predebug:server": "yarn run build",
"debug": "node --debug-brk dist/server.js",
"debug:server": "node-nightly --inspect --debug-brk dist/server.js",
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js",
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --config webpack.prod.config.ts",
"docs": "typedoc --options typedoc.json ./src/",
"lint": "tslint \"src/**/*.ts\" || true",
"global": "npm install -g @angular/cli nodemon npm-check-updates rimraf ts-node typedoc typescript webpack webpack-bundle-size-analyzer rollup marked node-gyp",
"ci": "yarn run lint && yarn run build:prod:ngc:json && yarn run test && npm-run-all -p -r server e2e",
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server -p",
"ci": "yarn run lint && yarn run build:aot && yarn run test && npm-run-all -p -r server e2e",
"protractor": "node node_modules/protractor/bin/protractor",
"pree2e": "yarn run webdriver:update",
"e2e": "yarn run protractor",
"test": "karma start --single-run",
"test:watch": "karma start --no-single-run --auto-watch",
"coverage": "http-server -c-1 -o -p 9875 ./coverage",
"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",
"lint": "tslint \"src/**/*.ts\" || true && tslint \"e2e/**/*.ts\" || true",
"docs": "typedoc --options typedoc.json ./src/",
"coverage": "http-server -c-1 -o -p 9875 ./coverage"
},
"dependencies": {
"@angular/common": "2.2.3",
"@angular/compiler": "2.2.3",
"@angular/compiler-cli": "2.2.3",
"@angular/core": "2.2.3",
"@angular/forms": "2.2.3",
"@angular/http": "2.2.3",
"@angular/platform-browser": "2.2.3",
"@angular/platform-browser-dynamic": "2.2.3",
"@angular/platform-server": "2.2.3",
"@angular/router": "3.2.3",
"@angular/upgrade": "2.2.3",
"@angular/animations": "4.3.1",
"@angular/common": "4.3.1",
"@angular/core": "4.3.1",
"@angular/forms": "4.3.1",
"@angular/http": "4.3.1",
"@angular/platform-browser": "4.3.1",
"@angular/platform-browser-dynamic": "4.3.1",
"@angular/platform-server": "4.3.1",
"@angular/router": "4.3.1",
"@angularclass/bootloader": "1.0.1",
"@angularclass/idle-preload": "1.0.4",
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.18",
"@ngrx/core": "^1.2.0",
"@ngrx/effects": "2.0.2",
"@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",
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.28",
"@ngrx/core": "1.2.0",
"@ngrx/effects": "2.0.4",
"@ngrx/router-store": "1.2.6",
"@ngrx/store": "2.2.3",
"@nguniversal/express-engine": "1.0.0-beta.2",
"@ngx-translate/core": "7.1.0",
"@ngx-translate/http-loader": "0.1.0",
"body-parser": "1.17.2",
"bootstrap": "4.0.0-alpha.6",
"cerialize": "^0.1.13",
"compression": "1.6.2",
"express": "4.14.0",
"cerialize": "0.1.15",
"compression": "1.7.0",
"cookie-parser": "1.4.3",
"core-js": "2.4.1",
"express": "4.15.3",
"express-session": "1.15.4",
"font-awesome": "4.7.0",
"http-server": "^0.9.0",
"http-server": "0.10.0",
"https": "1.0.0",
"js.clone": "0.0.3",
"jsonschema": "^1.1.1",
"jsonschema": "1.1.1",
"methods": "1.1.2",
"morgan": "1.7.0",
"ng2-pagination": "^2.0.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",
"zone.js": "0.6.26"
"morgan": "1.8.2",
"ngx-pagination": "3.0.1",
"pem": "1.9.7",
"reflect-metadata": "0.1.10",
"rxjs": "5.4.2",
"ts-md5": "1.2.0",
"webfontloader": "1.6.28",
"zone.js": "0.8.14"
},
"devDependencies": {
"@ngtools/webpack": "1.1.9",
"@types/body-parser": "0.0.33",
"@types/compression": "0.0.33",
"@angular/compiler": "4.3.1",
"@angular/compiler-cli": "4.3.1",
"@ngrx/store-devtools": "3.2.4",
"@ngtools/webpack": "1.5.1",
"@types/cookie-parser": "1.3.30",
"@types/deep-freeze": "0.0.29",
"@types/express": "4.0.34",
"@types/express-serve-static-core": "4.0.39",
"@types/hammerjs": "2.0.33",
"@types/jasmine": "2.5.41",
"@types/deep-freeze": "0.1.1",
"@types/express": "4.0.36",
"@types/express-serve-static-core": "4.0.49",
"@types/hammerjs": "2.0.34",
"@types/jasmine": "2.5.53",
"@types/lodash": "ts2.0",
"@types/memory-cache": "0.0.29",
"@types/mime": "0.0.29",
"@types/morgan": "1.7.32",
"@types/node": "6.0.52",
"@types/mime": "1.3.1",
"@types/node": "8.0.14",
"@types/serve-static": "1.7.31",
"@types/webfontloader": "1.6.27",
"ajv": "4.2.0",
"ajv-keywords": "1.1.1",
"angular2-template-loader": "0.6.0",
"autoprefixer": "6.5.4",
"awesome-typescript-loader": "2.2.4",
"codelyzer": "2.0.0-beta.3",
"cookie-parser": "1.4.3",
"@types/source-map": "0.5.0",
"@types/webfontloader": "1.6.28",
"ajv": "5.2.2",
"ajv-keywords": "2.1.0",
"angular2-template-loader": "0.6.2",
"autoprefixer": "7.1.2",
"awesome-typescript-loader": "3.2.1",
"codelyzer": "3.1.2",
"compression-webpack-plugin": "1.0.0",
"copy-webpack-plugin": "4.0.1",
"css-loader": "^0.26.0",
"css-loader": "0.28.4",
"deep-freeze": "0.0.1",
"html-webpack-plugin": "^2.21.0",
"imports-loader": "0.7.0",
"istanbul-instrumenter-loader": "^0.2.0",
"jasmine-core": "~2.5.2",
"jasmine-spec-reporter": "~2.7.0",
"exports-loader": "0.6.4",
"html-webpack-plugin": "2.29.0",
"imports-loader": "0.7.1",
"istanbul-instrumenter-loader": "2.0.0",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"json-loader": "0.5.4",
"karma": "^1.2.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.0.2",
"karma-mocha-reporter": "^2.0.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-remap-istanbul": "^0.2.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-webdriver-launcher": "^1.0.4",
"karma-webpack": "1.8.0",
"ngrx-store-freeze": "^0.1.9",
"node-sass": "4.0.0",
"karma": "1.7.0",
"karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1",
"karma-coverage": "1.1.1",
"karma-istanbul-preprocessor": "0.0.2",
"karma-jasmine": "1.1.0",
"karma-mocha-reporter": "2.2.3",
"karma-phantomjs-launcher": "1.0.4",
"karma-remap-coverage": "0.1.4",
"karma-remap-istanbul": "0.6.0",
"karma-sourcemap-loader": "0.3.7",
"karma-webdriver-launcher": "1.0.5",
"karma-webpack": "2.0.4",
"ngrx-store-freeze": "0.1.9",
"node-sass": "4.5.3",
"nodemon": "1.11.0",
"npm-run-all": "4.0.2",
"postcss-cli": "^2.6.0",
"protractor": "~4.0.14",
"protractor-istanbul-plugin": "~2.0.0",
"postcss": "6.0.8",
"postcss-apply": "0.8.0",
"postcss-cli": "4.1.0",
"postcss-cssnext": "3.0.2",
"postcss-loader": "2.0.6",
"postcss-responsive-type": "1.0.0",
"postcss-smart-import": "0.7.5",
"protractor": "5.1.2",
"protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1",
"rimraf": "2.5.4",
"rollup": "0.37.0",
"rollup-plugin-commonjs": "6.0.0",
"resolve-url-loader": "2.1.0",
"rimraf": "2.6.1",
"rollup": "0.45.2",
"rollup-plugin-commonjs": "8.0.2",
"rollup-plugin-node-globals": "1.1.0",
"rollup-plugin-node-resolve": "2.0.0",
"rollup-plugin-uglify": "1.0.1",
"source-map-loader": "^0.1.5",
"string-replace-loader": "1.0.5",
"to-string-loader": "^1.1.4",
"rollup-plugin-node-resolve": "3.0.0",
"rollup-plugin-uglify": "2.0.1",
"sass-loader": "6.0.6",
"script-ext-html-webpack-plugin": "1.8.5",
"source-map-loader": "0.2.1",
"string-replace-loader": "1.3.0",
"to-string-loader": "1.1.5",
"ts-helpers": "1.1.2",
"ts-node": "1.7.2",
"tslint": "4.0.2",
"tslint-loader": "3.3.0",
"typedoc": "0.5.7",
"typescript": "2.0.10",
"v8-lazy-parse-webpack-plugin": "0.3.0",
"webpack": "2.1.0-beta.27",
"webpack-bundle-analyzer": "1.4.1",
"webpack-dev-middleware": "1.9.0",
"webpack-dev-server": "2.1.0-beta.11",
"webpack-merge": "1.1.1"
"ts-node": "3.2.0",
"tslint": "5.1.0",
"tslint-loader": "3.5.3",
"typedoc": "0.7.1",
"typescript": "2.4.1",
"webpack": "3.3.0",
"webpack-bundle-analyzer": "2.8.3",
"webpack-dev-middleware": "1.11.0",
"webpack-dev-server": "2.5.1",
"webpack-merge": "4.1.0",
"webpack-node-externals": "1.6.0"
}
}

8
postcss.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
plugins: [
require('postcss-smart-import')(),
require('postcss-cssnext')(),
require('postcss-apply')(),
require('postcss-responsive-type')()
]
};

View File

@@ -1,9 +0,0 @@
{
"use": ["autoprefixer"],
"input": "src/**/*.css",
"replace": true,
"local-plugins": true,
"autoprefixer": {
"browsers": "last 2 versions"
}
}

View File

@@ -2,7 +2,7 @@
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
/*global jasmine */
var SpecReporter = require('jasmine-spec-reporter');
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
exports.config = {
allScriptsTimeout: 11000,
@@ -28,11 +28,11 @@ exports.config = {
// -----------------------------------------------------------------
// Browser and Capabilities: Chrome
// -----------------------------------------------------------------
capabilities: {
'browserName': 'chrome',
'version': '',
'platform': 'ANY'
},
capabilities: {
'browserName': 'chrome',
'version': '',
'platform': 'ANY'
},
// -----------------------------------------------------------------
// Browser and Capabilities: Firefox
// -----------------------------------------------------------------
@@ -63,7 +63,7 @@ exports.config = {
// }
//],
plugins : [{
plugins: [{
path: 'node_modules/protractor-istanbul-plugin'
}],
@@ -71,15 +71,19 @@ exports.config = {
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
print: function () {}
},
useAllAngular2AppRoots: true,
beforeLaunch: function() {
beforeLaunch: function () {
require('ts-node').register({
project: 'e2e'
});
},
onPrepare: function() {
jasmine.getEnv().addReporter(new SpecReporter());
onPrepare: function () {
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: true
}
}));
}
};

0
resources/data/.gitkeep Normal file
View File

View File

View File

@@ -0,0 +1,5 @@
{
"test": {
"message": "Hello, DSpace!"
}
}

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,16 +0,0 @@
import rollup from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify'
export default {
entry : 'dist/client/main.bundle.js',
dest : 'dist/client/main.bundle.js',
sourceMap : false,
format : 'iife',
plugins : [
nodeResolve({jsnext: true, module: true}),
commonjs({include: 'node_modules/rxjs/**'}),
uglify()
]
}

View File

@@ -1,16 +0,0 @@
import rollup from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify'
export default {
entry : 'dist/server/index.js',
dest : 'dist/server/index.js',
sourceMap : false,
format : 'iife',
plugins : [
nodeResolve({jsnext: true, module: true}),
commonjs({include: 'node_modules/rxjs/**'}),
uglify()
]
}

20
rollup.config.js Normal file
View File

@@ -0,0 +1,20 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify'
export default {
entry: 'dist/client.js',
dest: 'dist/client.js',
sourceMap: false,
format: 'iife',
plugins: [
nodeResolve({
jsnext: true,
module: true
}),
commonjs({
include: 'node_modules/rxjs/**'
}),
uglify()
]
}

View File

@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
@NgModule({
imports: [
RouterModule.forChild([

View File

@@ -1,4 +1,13 @@
@import '../styles/variables.scss';
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
@import "../../node_modules/font-awesome/scss/font-awesome.scss";
html {
position: relative;
min-height: 100%;
}
// Sticky Footer
.outer-wrapper {
display: flex;
margin: 0;

View File

@@ -5,24 +5,33 @@ import {
inject,
TestBed
} from '@angular/core/testing';
import {
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from "@angular/core";
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { By } from '@angular/platform-browser';
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
import { Store, StoreModule } from "@ngrx/store";
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store';
// Load the implementations that should be tested
import { AppComponent } from './app.component';
import { CommonModule } from '@angular/common';
import { HostWindowState } from "./shared/host-window.reducer";
import { HostWindowResizeAction } from "./shared/host-window.actions";
import { MockTranslateLoader } from "./shared/testing/mock-translate-loader";
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { MockTranslateLoader } from './shared/testing/mock-translate-loader';
import { GLOBAL_CONFIG, EnvConfig } from '../config';
import { NativeWindowRef, NativeWindowService } from "./shared/window.service";
import { BrowserCookiesModule } from '../modules/cookies/browser-cookies.module';
import { BrowserDataLoaderModule } from '../modules/data-loader/browser-data-loader.module';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module';
import { BrowserTransferStoreModule } from '../modules/transfer-store/browser-transfer-store.module';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
@@ -34,15 +43,23 @@ describe('App component', () => {
// async beforeEach
beforeEach(async(() => {
return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})],
imports: [
CommonModule,
StoreModule.provideStore({}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
BrowserCookiesModule,
BrowserDataLoaderModule,
BrowserTransferStateModule,
BrowserTransferStoreModule
],
declarations: [AppComponent], // declare the test component
providers: [
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
AppComponent
],
@@ -56,7 +73,7 @@ describe('App component', () => {
comp = fixture.componentInstance; // component test instance
// query for the <div class="outer-wrapper"> by CSS element selector
// query for the <div class='outer-wrapper'> by CSS element selector
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
el = de.nativeElement;
});
@@ -66,7 +83,7 @@ describe('App component', () => {
expect(app).toBeTruthy();
}));
describe("when the window is resized", () => {
describe('when the window is resized', () => {
let width: number;
let height: number;
let store: Store<HostWindowState>;
@@ -80,7 +97,7 @@ describe('App component', () => {
height = window.innerHeight;
});
it("should dispatch a HostWindowResizeAction with the width and height of the window as its payload", () => {
it('should dispatch a HostWindowResizeAction with the width and height of the window as its payload', () => {
expect(store.dispatch).toHaveBeenCalledWith(new HostWindowResizeAction(width, height));
});

View File

@@ -1,31 +1,40 @@
import {
Component,
ChangeDetectionStrategy,
Component,
Inject,
ViewEncapsulation,
OnInit, HostListener
} from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { HostWindowState } from "./shared/host-window.reducer";
import { Store } from "@ngrx/store";
OnInit,
HostListener
} from '@angular/core';
import { HostWindowResizeAction } from "./shared/host-window.actions";
import { EnvConfig, GLOBAL_CONFIG, GlobalConfig } from '../config';
import { NativeWindowRef, NativeWindowService } from "./shared/window.service";
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
@Component({
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated,
selector: 'ds-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
constructor(
@Inject(GLOBAL_CONFIG) public EnvConfig: GlobalConfig,
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
@Inject(NativeWindowService) private _window: NativeWindowRef,
private translate: TranslateService,
private cache: TransferState,
private store: Store<HostWindowState>
) {
// this language will be used as a fallback when a translation isn't found in the current language
@@ -34,11 +43,20 @@ export class AppComponent implements OnInit {
translate.use('en');
}
ngAfterViewChecked() {
this.syncCache();
}
syncCache() {
this.store.take(1).subscribe((state: HostWindowState) => {
this.cache.set('state', state);
});
}
ngOnInit() {
this.onInit();
const env: string = EnvConfig.production ? "Production" : "Development";
const color: string = EnvConfig.production ? "red" : "green";
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
const env: string = this.config.production ? 'Production' : 'Development';
const color: string = this.config.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
}
@HostListener('window:resize', ['$event'])
@@ -48,12 +66,4 @@ export class AppComponent implements OnInit {
);
}
private onInit(): void {
if (typeof this._window !== 'undefined') {
this.store.dispatch(
new HostWindowResizeAction(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight)
);
}
}
}

View File

@@ -1,8 +1,11 @@
import { EffectsModule } from "@ngrx/effects";
import { HeaderEffects } from "./header/header.effects";
import { coreEffects } from "./core/core.effects";
import { EffectsModule } from '@ngrx/effects';
import { HeaderEffects } from './header/header.effects';
import { StoreEffects } from './store.effects';
import { coreEffects } from './core/core.effects';
export const effects = [
...coreEffects, //TODO should probably be imported in coreModule
...coreEffects, // TODO: should probably be imported in coreModule
EffectsModule.run(StoreEffects),
EffectsModule.run(HeaderEffects)
];

View File

@@ -1,38 +1,63 @@
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { StoreModule, Store } from '@ngrx/store';
import { RouterStoreModule } from '@ngrx/router-store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { rootReducer, AppState } from './app.reducer';
import { effects } from './app.effects';
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';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module';
import { HomeModule } from './home/home.module';
import { ItemPageModule } from './item-page/item-page.module';
import { CollectionPageModule } from './collection-page/collection-page.module';
import { CommunityPageModule } from './community-page/community-page.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
export function getConfig() {
return ENV_CONFIG;
}
@NgModule({
imports: [
SharedModule,
FormsModule,
CoreModule.forRoot(),
HttpModule,
TransferHttpModule,
HomeModule,
ItemPageModule,
CollectionPageModule,
CommunityPageModule,
AppRoutingModule,
StoreModule.provideStore(rootReducer),
RouterStoreModule.connectRouter(),
StoreDevtoolsModule.instrumentOnlyWithExtension(),
effects
],
providers: [
{ provide: GLOBAL_CONFIG, useFactory: (getConfig) },
],
declarations: [
AppComponent,
HeaderComponent,
PageNotFoundComponent,
],
imports: [
SharedModule,
HomeModule,
ItemPageModule,
CollectionPageModule,
CommunityPageModule,
CoreModule.forRoot(),
AppRoutingModule
],
providers: [
]
exports: [AppComponent]
})
export class AppModule {
}
export { AppComponent } from './app.component';
}

45
src/app/app.reducer.ts Normal file
View File

@@ -0,0 +1,45 @@
import { combineReducers, ActionReducer } from '@ngrx/store';
import { routerReducer, RouterState } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
import { compose } from '@ngrx/core';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { CoreState, coreReducer } from './core/core.reducers';
import { StoreActionTypes } from './store.actions';
import { ENV_CONFIG } from '../config';
export interface AppState {
core: CoreState;
router: RouterState;
hostWindow: HostWindowState;
header: HeaderState;
}
export const reducers = {
core: coreReducer,
router: routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer
};
export function rootReducer(state: any, action: any) {
switch (action.type) {
case StoreActionTypes.REHYDRATE:
state = Object.assign({}, state, action.payload);
break;
case StoreActionTypes.REPLAY:
break;
default:
}
let root: ActionReducer<any>;
// TODO: attempt to not use InjectionToken GLOBAL_CONFIG over GlobalConfig ENV_CONFIG
if (ENV_CONFIG.production) {
root = combineReducers(reducers)(state, action);
} else {
root = compose(storeFreeze, combineReducers)(reducers)(state, action);
}
return root;
}

View File

@@ -1,39 +0,0 @@
import { combineReducers } from "@ngrx/store";
import { routerReducer, RouterState } from "@ngrx/router-store";
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from "./shared/host-window.reducer";
import { CoreState, coreReducer } from "./core/core.reducers";
import { storeFreeze } from 'ngrx-store-freeze';
import { compose } from "@ngrx/core";
import { StoreActionTypes } from "./store.actions";
import { EnvConfig } from '../config';
export interface AppState {
core: CoreState;
router: RouterState;
hostWindow: HostWindowState;
header: HeaderState;
}
export const reducers = {
core: coreReducer,
router: routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer
};
export function rootReducer(state: any, action: any) {
let output;
if (action.type === StoreActionTypes.REHYDRATE) {
state = action.payload;
}
if (EnvConfig.production) {
output = combineReducers(reducers)(state, action);
} else {
output = compose(storeFreeze, combineReducers)(reducers)(state, action);
}
return output;
}
export const NGRX_CACHE_KEY = "NGRX_STORE";

View File

@@ -0,0 +1,76 @@
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { Http } from '@angular/http';
import { RouterModule } from '@angular/router';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { BrowserCookiesModule } from '../modules/cookies/browser-cookies.module';
import { BrowserDataLoaderModule } from '../modules/data-loader/browser-data-loader.module';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module';
import { BrowserTransferStoreEffects } from '../modules/transfer-store/browser-transfer-store.effects';
import { BrowserTransferStoreModule } from '../modules/transfer-store/browser-transfer-store.module';
import { SharedModule } from './shared/shared.module';
import { CoreModule } from './core/core.module';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
export function init(cache: TransferState) {
return () => {
cache.initialize();
};
}
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'assets/i18n/', '.json');
}
@NgModule({
bootstrap: [AppComponent],
imports: [
BrowserModule.withServerTransition({
appId: 'ds-app-id'
}),
IdlePreloadModule.forRoot(), // forRoot ensures the providers are only created once
RouterModule.forRoot([], { useHash: false, preloadingStrategy: IdlePreload }),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [Http]
}
}),
NgbModule.forRoot(),
BrowserCookiesModule,
BrowserDataLoaderModule,
BrowserTransferStateModule,
BrowserTransferStoreModule,
EffectsModule.run(BrowserTransferStoreEffects),
BrowserAnimationsModule,
AppModule
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useFactory: init,
deps: [
TransferState
]
}
]
})
export class BrowserAppModule {
}

View File

@@ -4,21 +4,22 @@ import {
} from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Collection } from "../core/shared/collection.model";
import { Bitstream } from "../core/shared/bitstream.model";
import { RemoteData } from "../core/data/remote-data";
import { CollectionDataService } from "../core/data/collection-data.service";
import { Subscription } from "rxjs/Subscription";
import { ItemDataService } from "../core/data/item-data.service";
import { Item } from "../core/shared/item.model";
import { SortOptions, SortDirection } from "../core/cache/models/sort-options.model";
import { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
import { Observable } from "rxjs/Observable";
import { hasValue } from "../shared/empty.util";
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Collection } from '../core/shared/collection.model';
import { Bitstream } from '../core/shared/bitstream.model';
import { RemoteData } from '../core/data/remote-data';
import { CollectionDataService } from '../core/data/collection-data.service';
import { ItemDataService } from '../core/data/item-data.service';
import { Item } from '../core/shared/item.model';
import { SortOptions, SortDirection } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { hasValue } from '../shared/empty.util';
@Component({
selector: 'ds-collection-page',
styleUrls: ['./collection-page.component.css'],
styleUrls: ['./collection-page.component.scss'],
templateUrl: './collection-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
@@ -26,8 +27,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
collectionData: RemoteData<Collection>;
itemData: RemoteData<Item[]>;
logoData: RemoteData<Bitstream>;
config : PaginationComponentOptions;
sortConfig : SortOptions;
config: PaginationComponentOptions;
sortConfig: SortOptions;
private subs: Subscription[] = [];
private collectionId: string;
@@ -37,22 +38,21 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
private ref: ChangeDetectorRef,
private route: ActivatedRoute
) {
this.universalInit();
}
ngOnInit(): void {
this.subs.push(this.route.params.map((params: Params) => params['id'] )
this.subs.push(this.route.params.map((params: Params) => params.id)
.subscribe((id: string) => {
this.collectionId = id;
this.collectionData = this.collectionDataService.findById(this.collectionId);
this.subs.push(this.collectionData.payload
.subscribe(collection => this.logoData = collection.logo));
this.subs.push(this.collectionData.payload.subscribe((collection) => this.logoData = collection.logo));
this.config = new PaginationComponentOptions();
this.config.id = "collection-browse";
this.config.pageSizeOptions = [ 4 ];
this.config.id = 'collection-browse';
this.config.pageSizeOptions = [4];
this.config.pageSize = 4;
this.sortConfig = new SortOptions();
this.sortConfig = new SortOptions();
this.updateResults();
}));
@@ -60,12 +60,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.subs
.filter(sub => hasValue(sub))
.forEach(sub => sub.unsubscribe());
}
universalInit() {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
onPageChange(currentPage: number): void {

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from "@ngx-translate/core";
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../shared/shared.module';

View File

@@ -2,17 +2,17 @@
<!-- Community name -->
<ds-comcol-page-header [name]="(communityData.payload | async)?.name"></ds-comcol-page-header>
<!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoData"
<ds-comcol-page-logo *ngIf="logoData"
[logo]="logoData.payload | async"
[alternateText]="'Community Logo'">
</ds-comcol-page-logo>
<!-- Introductionary text -->
<ds-comcol-page-content
<ds-comcol-page-content
[content]="(communityData.payload | async)?.introductoryText"
[hasInnerHtml]="true">
</ds-comcol-page-content>
<!-- News -->
<ds-comcol-page-content
<ds-comcol-page-content
[content]="(communityData.payload | async)?.sidebarText"
[hasInnerHtml]="true"
[title]="'community.page.news'">
@@ -21,6 +21,6 @@
<ds-comcol-page-content
[content]="(communityData.payload | async)?.copyrightText"
[hasInnerHtml]="true">
</ds-comcol-page-content>
</ds-comcol-page-content>
<ds-community-page-sub-collection-list></ds-community-page-sub-collection-list>
</div>

View File

@@ -1,16 +1,17 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Community } from "../core/shared/community.model";
import { Bitstream } from "../core/shared/bitstream.model";
import { RemoteData } from "../core/data/remote-data";
import { CommunityDataService } from "../core/data/community-data.service";
import { Subscription } from "rxjs/Subscription";
import { hasValue } from "../shared/empty.util";
import { Subscription } from 'rxjs/Subscription';
import { Community } from '../core/shared/community.model';
import { Bitstream } from '../core/shared/bitstream.model';
import { RemoteData } from '../core/data/remote-data';
import { CommunityDataService } from '../core/data/community-data.service';
import { hasValue } from '../shared/empty.util';
@Component({
selector: 'ds-community-page',
styleUrls: ['./community-page.component.css'],
styleUrls: ['./community-page.component.scss'],
templateUrl: './community-page.component.html',
})
export class CommunityPageComponent implements OnInit, OnDestroy {
@@ -22,23 +23,21 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
private communityDataService: CommunityDataService,
private route: ActivatedRoute
) {
this.universalInit();
}
ngOnInit(): void {
this.route.params.subscribe((params: Params) => {
this.communityData = this.communityDataService.findById(params['id']);
this.communityData = this.communityDataService.findById(params.id);
this.subs.push(this.communityData.payload
.subscribe(community => this.logoData = community.logo));
.subscribe((community) => this.logoData = community.logo));
});
}
ngOnDestroy(): void {
this.subs
.filter(sub => hasValue(sub))
.forEach(sub => sub.unsubscribe());
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
universalInit() {
}
}

View File

@@ -1,8 +1,8 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from "@angular/router";
import { RouterModule } from '@angular/router';
import { TranslateModule } from "@ngx-translate/core";
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../shared/shared.module';
import { CommunityPageComponent } from './community-page.component';

View File

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

View File

@@ -1,28 +1,22 @@
import { Component, OnInit } from '@angular/core';
import { CollectionDataService } from "../../core/data/collection-data.service";
import { RemoteData } from "../../core/data/remote-data";
import { Collection } from "../../core/shared/collection.model";
import { CollectionDataService } from '../../core/data/collection-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
@Component({
selector: 'ds-community-page-sub-collection-list',
styleUrls: ['./community-page-sub-collection-list.component.css'],
templateUrl: './community-page-sub-collection-list.component.html',
selector: 'ds-community-page-sub-collection-list',
styleUrls: ['./community-page-sub-collection-list.component.scss'],
templateUrl: './community-page-sub-collection-list.component.html',
})
export class CommunityPageSubCollectionListComponent implements OnInit {
subCollections: RemoteData<Collection[]>;
subCollections: RemoteData<Collection[]>;
constructor(
private cds: CollectionDataService
) {
this.universalInit();
}
constructor(private cds: CollectionDataService) {
universalInit() {
}
}
ngOnInit(): void {
this.subCollections = this.cds.findAll();
}
ngOnInit(): void {
this.subCollections = this.cds.findAll();
}
}

View File

@@ -1,28 +1,29 @@
import "reflect-metadata";
import { GenericConstructor } from "../../shared/generic-constructor";
import { CacheableObject } from "../object-cache.reducer";
import { ResourceType } from "../../shared/resource-type";
import 'reflect-metadata';
const mapsToMetadataKey = Symbol("mapsTo");
const relationshipKey = Symbol("relationship");
import { GenericConstructor } from '../../shared/generic-constructor';
import { CacheableObject } from '../object-cache.reducer';
import { ResourceType } from '../../shared/resource-type';
const mapsToMetadataKey = Symbol('mapsTo');
const relationshipKey = Symbol('relationship');
const relationshipMap = new Map();
export const mapsTo = function(value: GenericConstructor<CacheableObject>) {
export function mapsTo(value: GenericConstructor<CacheableObject>) {
return Reflect.metadata(mapsToMetadataKey, value);
};
}
export const getMapsTo = function(target: any) {
export function getMapsTo(target: any) {
return Reflect.getOwnMetadata(mapsToMetadataKey, target);
};
}
export const relationship = function(value: ResourceType, isList: boolean = false): any {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
export function relationship(value: ResourceType, isList: boolean = false): any {
return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!target || !propertyKey) {
return;
}
let metaDataList : Array<string> = relationshipMap.get(target.constructor) || [];
const metaDataList: string[] = relationshipMap.get(target.constructor) || [];
if (metaDataList.indexOf(propertyKey) === -1) {
metaDataList.push(propertyKey);
}
@@ -30,12 +31,12 @@ export const relationship = function(value: ResourceType, isList: boolean = fals
return Reflect.metadata(relationshipKey, { resourceType: value, isList }).apply(this, arguments);
};
};
}
export const getRelationMetadata = function(target: any, propertyKey: string) {
export function getRelationMetadata(target: any, propertyKey: string) {
return Reflect.getMetadata(relationshipKey, target, propertyKey);
};
}
export const getRelationships = function(target: any) {
export function getRelationships(target: any) {
return relationshipMap.get(target);
};
}

View File

@@ -1,20 +1,21 @@
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, getRelationMetadata, getRelationships } from "./build-decorators";
import { NormalizedObjectFactory } from "../models/normalized-object-factory";
import { Request } from "../../data/request.models";
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
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 { 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 { RemoteData } from '../../data/remote-data';
import { GenericConstructor } from '../../shared/generic-constructor';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
import { Request } from '../../data/request.models';
@Injectable()
export class RemoteDataBuildService {
@@ -33,14 +34,14 @@ export class RemoteDataBuildService {
const requestHrefObs = this.objectCache.getRequestHrefBySelfLink(href);
const requestObs = Observable.race(
this.store.select<RequestEntry>('core', 'data', 'request', href).filter(entry => hasValue(entry)),
requestHrefObs.flatMap(requestHref =>
this.store.select<RequestEntry>('core', 'data', 'request', requestHref)).filter(entry => hasValue(entry))
this.store.select<RequestEntry>('core', 'data', 'request', href).filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) =>
this.store.select<RequestEntry>('core', 'data', 'request', requestHref)).filter((entry) => hasValue(entry))
);
const responseCacheObs = Observable.race(
this.responseCache.get(href).filter(entry => hasValue(entry)),
requestHrefObs.flatMap(requestHref => this.responseCache.get(requestHref)).filter(entry => hasValue(entry))
this.responseCache.get(href).filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
);
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged();
@@ -52,30 +53,31 @@ export class RemoteDataBuildService {
const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage)
.distinctUntilChanged();
const statusCode = responseCacheObs
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
.distinctUntilChanged();
/* tslint:disable:no-string-literal */
const pageInfo = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).pageInfo)
.distinctUntilChanged();
/* tslint:enable:no-string-literal */
//always use self link if that is cached, only if it isn't, get it via the response.
// always use self link if that is cached, only if it isn't, get it via the response.
const payload =
Observable.combineLatest(
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType).startWith(undefined),
responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
.flatMap((resourceUUIDs: string[]) => {
if (isNotEmpty(resourceUUIDs)) {
return this.objectCache.get(resourceUUIDs[0], normalizedType);
}
else {
} else {
return Observable.of(undefined);
}
})
@@ -84,16 +86,14 @@ export class RemoteDataBuildService {
(fromSelfLink, fromResponse) => {
if (hasValue(fromSelfLink)) {
return fromSelfLink;
}
else {
} else {
return fromResponse;
}
}
).filter(normalized => hasValue(normalized))
).filter((normalized) => hasValue(normalized))
.map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
});
});
return new RemoteData(
href,
@@ -112,8 +112,8 @@ export class RemoteDataBuildService {
normalizedType: GenericConstructor<TNormalized>
): RemoteData<TDomain[]> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href)
.filter(entry => hasValue(entry));
const responseCacheObs = this.responseCache.get(href).filter(entry => hasValue(entry));
.filter((entry) => hasValue(entry));
const responseCacheObs = this.responseCache.get(href).filter((entry) => hasValue(entry));
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged();
@@ -124,22 +124,24 @@ export class RemoteDataBuildService {
const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage)
.distinctUntilChanged();
const statusCode = responseCacheObs
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
.distinctUntilChanged();
/* tslint:disable:no-string-literal */
const pageInfo = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).pageInfo)
.distinctUntilChanged();
/* tslint:enable:no-string-literal */
const payload = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
.flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs, normalizedType)
.map((normList: TNormalized[]) => {
return normList.map((normalized: TNormalized) => {
@@ -161,9 +163,8 @@ export class RemoteDataBuildService {
);
}
build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain {
let links: any = {};
const links: any = {};
const relationships = getRelationships(normalized.constructor) || [];
@@ -180,24 +181,22 @@ export class RemoteDataBuildService {
});
}, 0);
let rdArr = [];
const rdArr = [];
normalized[relationship].forEach((href: string) => {
rdArr.push(this.buildSingle(href, resourceConstructor));
});
if (rdArr.length === 1) {
links[relationship] = rdArr[0];
}
else {
} else {
links[relationship] = this.aggregate(rdArr);
}
}
else {
} 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(new Request(normalized[relationship]));
},0);
}, 0);
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
@@ -215,51 +214,49 @@ export class RemoteDataBuildService {
return Object.assign(new domainModel(), normalized, links);
}
aggregate<T>(input: RemoteData<T>[]): RemoteData<T[]> {
aggregate<T>(input: Array<RemoteData<T>>): RemoteData<T[]> {
const requestPending = Observable.combineLatest(
...input.map(rd => rd.isRequestPending),
).map((...pendingArray) => pendingArray.every(e => e === true))
...input.map((rd) => rd.isRequestPending),
).map((...pendingArray) => pendingArray.every((e) => e === true))
.distinctUntilChanged();
const responsePending = Observable.combineLatest(
...input.map(rd => rd.isResponsePending),
).map((...pendingArray) => pendingArray.every(e => e === true))
...input.map((rd) => rd.isResponsePending),
).map((...pendingArray) => pendingArray.every((e) => e === true))
.distinctUntilChanged();
const isSuccessFul = Observable.combineLatest(
...input.map(rd => rd.hasSucceeded),
).map((...successArray) => successArray.every(e => e === true))
...input.map((rd) => rd.hasSucceeded),
).map((...successArray) => successArray.every((e) => e === true))
.distinctUntilChanged();
const errorMessage = Observable.combineLatest(
...input.map(rd => rd.errorMessage),
...input.map((rd) => rd.errorMessage),
).map((...errors) => errors
.map((e, idx) => {
if (hasValue(e)) {
return `[${idx}]: ${e}`;
}
})
.filter(e => hasValue(e))
.join(", ")
);
.filter((e) => hasValue(e))
.join(', ')
);
const statusCode = Observable.combineLatest(
...input.map(rd => rd.statusCode),
...input.map((rd) => rd.statusCode),
).map((...statusCodes) => statusCodes
.map((code, idx) => {
if (hasValue(code)) {
return `[${idx}]: ${code}`;
}
})
.filter(c => hasValue(c))
.join(", ")
);
.filter((c) => hasValue(c))
.join(', ')
);
const pageInfo = Observable.of(undefined);
const payload = <Observable<T[]>> Observable.combineLatest(
...input.map(rd => rd.payload)
);
const payload = Observable.combineLatest(...input.map((rd) => rd.payload)) as Observable<T[]>;
return new RemoteData(
// This is an aggregated object, it doesn't necessarily correspond
@@ -275,4 +272,5 @@ export class RemoteDataBuildService {
payload
);
}
}

View File

@@ -1,6 +1,7 @@
import { combineReducers } from "@ngrx/store";
import { ResponseCacheState, responseCacheReducer } from "./response-cache.reducer";
import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer";
import { combineReducers } from '@ngrx/store';
import { ResponseCacheState, responseCacheReducer } from './response-cache.reducer';
import { ObjectCacheState, objectCacheReducer } from './object-cache.reducer';
export interface CacheState {
response: ResponseCacheState,

View File

@@ -1,11 +1,13 @@
import { NormalizedObject } from "./normalized-object.model";
import { inheritSerialization } from "cerialize";
import { inheritSerialization } from 'cerialize';
import { NormalizedObject } from './normalized-object.model';
@inheritSerialization(NormalizedObject)
export class NormalizedBitstreamFormat extends NormalizedObject {
//TODO this class was created as a placeholder when we connected to the live rest api
// TODO: this class was created as a placeholder when we connected to the live rest api
get uuid(): string {
return this.self;
}
}

View File

@@ -1,8 +1,9 @@
import { inheritSerialization, autoserialize } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
import { Bitstream } from "../../shared/bitstream.model";
import { mapsTo, relationship } from "../builders/build-decorators";
import { ResourceType } from "../../shared/resource-type";
import { inheritSerialization, autoserialize } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Bitstream } from '../../shared/bitstream.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
@mapsTo(Bitstream)
@inheritSerialization(NormalizedDSpaceObject)
@@ -43,7 +44,7 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
*/
@autoserialize
@relationship(ResourceType.Item, true)
parents: Array<string>;
parents: string[];
/**
* The Bundle that owns this Bitstream
@@ -57,4 +58,5 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
*/
@autoserialize
bundleName: string;
}

View File

@@ -1,8 +1,9 @@
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 { ResourceType } from "../../shared/resource-type";
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 { ResourceType } from '../../shared/resource-type';
@mapsTo(Bundle)
@inheritSerialization(NormalizedDSpaceObject)
@@ -17,7 +18,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
/**
* An array of Items that are direct parents of this Bundle
*/
parents: Array<string>;
parents: string[];
/**
* The Item that owns this Bundle
@@ -26,5 +27,6 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
@autoserialize
@relationship(ResourceType.Bitstream, true)
bitstreams: Array<string>;
bitstreams: string[];
}

View File

@@ -1,8 +1,9 @@
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 { ResourceType } from "../../shared/resource-type";
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 { ResourceType } from '../../shared/resource-type';
@mapsTo(Collection)
@inheritSerialization(NormalizedDSpaceObject)
@@ -26,7 +27,7 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
*/
@autoserialize
@relationship(ResourceType.Community, true)
parents: Array<string>;
parents: string[];
/**
* The Community that owns this Collection
@@ -37,6 +38,6 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
@autoserialize
@relationship(ResourceType.Item, true)
items: Array<string>;
items: string[];
}

View File

@@ -1,8 +1,9 @@
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 { ResourceType } from "../../shared/resource-type";
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 { ResourceType } from '../../shared/resource-type';
@mapsTo(Community)
@inheritSerialization(NormalizedDSpaceObject)
@@ -26,7 +27,7 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
*/
@autoserialize
@relationship(ResourceType.Community, true)
parents: Array<string>;
parents: string[];
/**
* The Community that owns this Community
@@ -37,6 +38,6 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
@autoserialize
@relationship(ResourceType.Collection, true)
collections: Array<string>;
collections: string[];
}

View File

@@ -1,12 +1,13 @@
import { autoserialize, autoserializeAs, inheritSerialization } from "cerialize";
import { Metadatum } from "../../shared/metadatum.model";
import { ResourceType } from "../../shared/resource-type";
import { NormalizedObject } from "./normalized-object.model";
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { Metadatum } from '../../shared/metadatum.model';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model';
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class NormalizedDSpaceObject extends NormalizedObject{
export abstract class NormalizedDSpaceObject extends NormalizedObject {
/**
* The link to the rest endpoint where this object can be found
@@ -51,17 +52,18 @@ export abstract class NormalizedDSpaceObject extends NormalizedObject{
* An array containing all metadata of this DSpaceObject
*/
@autoserializeAs(Metadatum)
metadata: Array<Metadatum>;
metadata: Metadatum[];
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
@autoserialize
parents: Array<string>;
parents: string[];
/**
* The DSpaceObject that owns this DSpaceObject
*/
@autoserialize
owner: string;
}

View File

@@ -1,8 +1,9 @@
import { inheritSerialization, autoserialize, autoserializeAs } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
import { Item } from "../../shared/item.model";
import { mapsTo, relationship } from "../builders/build-decorators";
import { ResourceType } from "../../shared/resource-type";
import { inheritSerialization, autoserialize, autoserializeAs } from 'cerialize';
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
import { Item } from '../../shared/item.model';
import { mapsTo, relationship } from '../builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
@mapsTo(Item)
@inheritSerialization(NormalizedDSpaceObject)
@@ -43,7 +44,7 @@ export class NormalizedItem extends NormalizedDSpaceObject {
*/
@autoserialize
@relationship(ResourceType.Collection, true)
parents: Array<string>;
parents: string[];
/**
* The Collection that owns this Item
@@ -53,5 +54,6 @@ export class NormalizedItem extends NormalizedDSpaceObject {
@autoserialize
@relationship(ResourceType.Bitstream, true)
bitstreams: Array<string>;
bitstreams: string[];
}

View File

@@ -1,13 +1,13 @@
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 { NormalizedCommunity } from "./normalized-community.model";
import { ResourceType } from "../../shared/resource-type";
import { NormalizedObject } from "./normalized-object.model";
import { NormalizedBitstreamFormat } from "./normalized-bitstream-format.model";
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 { NormalizedCommunity } from './normalized-community.model';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model';
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {

View File

@@ -1,5 +1,5 @@
import { CacheableObject } from "../object-cache.reducer";
import { autoserialize } from "cerialize";
import { CacheableObject } from '../object-cache.reducer';
import { autoserialize } from 'cerialize';
/**
* An abstract model class for a NormalizedObject.
*/

View File

@@ -1,4 +1,4 @@
import { autoserialize } from "cerialize";
import { autoserialize } from 'cerialize';
export class SelfLink {
@@ -7,4 +7,5 @@ export class SelfLink {
@autoserialize
uuid: string;
}

View File

@@ -5,7 +5,7 @@ export enum SortDirection {
export class SortOptions {
constructor (public field: string = "name", public direction : SortDirection = SortDirection.Ascending) {
constructor(public field: string = 'name', public direction: SortDirection = SortDirection.Ascending) {
}
}

View File

@@ -1,6 +1,7 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { CacheableObject } from "./object-cache.reducer";
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { CacheableObject } from './object-cache.reducer';
/**
* The list of ObjectCacheAction type definitions
@@ -11,6 +12,7 @@ export const ObjectCacheActionTypes = {
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS')
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add an object to the cache
*/
@@ -77,6 +79,7 @@ export class ResetObjectCacheTimestampsAction implements Action {
this.payload = newTimestamp;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all ObjectCacheActions

View File

@@ -1,9 +1,10 @@
import * as deepFreeze from "deep-freeze";
import { objectCacheReducer } from "./object-cache.reducer";
import * as deepFreeze from 'deep-freeze';
import { objectCacheReducer } from './object-cache.reducer';
import {
AddToObjectCacheAction,
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
} from "./object-cache.actions";
} from './object-cache.actions';
class NullAction extends RemoveFromObjectCacheAction {
type = null;
@@ -14,51 +15,51 @@ class NullAction extends RemoveFromObjectCacheAction {
}
}
describe("objectCacheReducer", () => {
describe('objectCacheReducer', () => {
const uuid1 = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const uuid2 = '28b04544-1766-4e82-9728-c4e93544ecd3';
const testState = {
[uuid1]: {
data: {
uuid: uuid1,
foo: "bar"
foo: 'bar'
},
timeAdded: new Date().getTime(),
msToLive: 900000,
requestHref: "https://rest.api/endpoint/uuid1"
requestHref: 'https://rest.api/endpoint/uuid1'
},
[uuid2]: {
data: {
uuid: uuid2,
foo: "baz"
foo: 'baz'
},
timeAdded: new Date().getTime(),
msToLive: 900000,
requestHref: "https://rest.api/endpoint/uuid2"
requestHref: 'https://rest.api/endpoint/uuid2'
}
};
deepFreeze(testState);
it("should return the current state when no valid actions have been made", () => {
it('should return the current state when no valid actions have been made', () => {
const action = new NullAction();
const newState = objectCacheReducer(testState, action);
expect(newState).toEqual(testState);
});
it("should start with an empty cache", () => {
it('should start with an empty cache', () => {
const action = new NullAction();
const initialState = objectCacheReducer(undefined, action);
expect(initialState).toEqual(Object.create(null));
});
it("should add the payload to the cache in response to an ADD action", () => {
it('should add the payload to the cache in response to an ADD action', () => {
const state = Object.create(null);
const objectToCache = {uuid: uuid1};
const objectToCache = { uuid: uuid1 };
const timeAdded = new Date().getTime();
const msToLive = 900000;
const requestHref = "https://rest.api/endpoint/uuid1";
const requestHref = 'https://rest.api/endpoint/uuid1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(state, action);
@@ -67,31 +68,33 @@ describe("objectCacheReducer", () => {
expect(newState[uuid1].msToLive).toEqual(msToLive);
});
it("should overwrite an object in the cache in response to an ADD action if it already exists", () => {
const objectToCache = {uuid: uuid1, foo: "baz", somethingElse: true};
it('should overwrite an object in the cache in response to an ADD action if it already exists', () => {
const objectToCache = { uuid: uuid1, foo: 'baz', somethingElse: true };
const timeAdded = new Date().getTime();
const msToLive = 900000;
const requestHref = "https://rest.api/endpoint/uuid1";
const requestHref = 'https://rest.api/endpoint/uuid1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(testState, action);
expect(newState[uuid1].data['foo']).toBe("baz");
/* tslint:disable:no-string-literal */
expect(newState[uuid1].data['foo']).toBe('baz');
expect(newState[uuid1].data['somethingElse']).toBe(true);
/* tslint:enable:no-string-literal */
});
it("should perform the ADD action without affecting the previous state", () => {
it('should perform the ADD action without affecting the previous state', () => {
const state = Object.create(null);
const objectToCache = {uuid: uuid1};
const objectToCache = { uuid: uuid1 };
const timeAdded = new Date().getTime();
const msToLive = 900000;
const requestHref = "https://rest.api/endpoint/uuid1";
const requestHref = 'https://rest.api/endpoint/uuid1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
deepFreeze(state);
objectCacheReducer(state, action);
});
it("should remove the specified object from the cache in response to the REMOVE action", () => {
it('should remove the specified object from the cache in response to the REMOVE action', () => {
const action = new RemoveFromObjectCacheAction(uuid1);
const newState = objectCacheReducer(testState, action);
@@ -108,13 +111,13 @@ describe("objectCacheReducer", () => {
expect(newState).toEqual(testState);
});
it("should perform the REMOVE action without affecting the previous state", () => {
it('should perform the REMOVE action without affecting the previous state', () => {
const action = new RemoveFromObjectCacheAction(uuid1);
//testState has already been frozen above
// testState has already been frozen above
objectCacheReducer(testState, action);
});
it("should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action", () => {
it('should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action', () => {
const newTimestamp = new Date().getTime();
const action = new ResetObjectCacheTimestampsAction(newTimestamp);
const newState = objectCacheReducer(testState, action);
@@ -123,9 +126,9 @@ describe("objectCacheReducer", () => {
});
});
it("should perform the RESET_TIMESTAMPS action without affecting the previous state", () => {
it('should perform the RESET_TIMESTAMPS action without affecting the previous state', () => {
const action = new ResetObjectCacheTimestampsAction(new Date().getTime());
//testState has already been frozen above
// testState has already been frozen above
objectCacheReducer(testState, action);
});

View File

@@ -1,9 +1,9 @@
import {
ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
} from "./object-cache.actions";
import { hasValue } from "../../shared/empty.util";
import { CacheEntry } from "./cache-entry";
} from './object-cache.actions';
import { hasValue } from '../../shared/empty.util';
import { CacheEntry } from './cache-entry';
/**
* An interface to represent objects that can be cached
@@ -52,15 +52,15 @@ export const objectCacheReducer = (state = initialState, action: ObjectCacheActi
switch (action.type) {
case ObjectCacheActionTypes.ADD: {
return addToObjectCache(state, <AddToObjectCacheAction>action);
return addToObjectCache(state, action as AddToObjectCacheAction);
}
case ObjectCacheActionTypes.REMOVE: {
return removeFromObjectCache(state, <RemoveFromObjectCacheAction>action)
return removeFromObjectCache(state, action as RemoveFromObjectCacheAction)
}
case ObjectCacheActionTypes.RESET_TIMESTAMPS: {
return resetObjectCacheTimestamps(state, <ResetObjectCacheTimestampsAction>action)
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
}
default: {
@@ -102,12 +102,11 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio
*/
function removeFromObjectCache(state: ObjectCacheState, action: RemoveFromObjectCacheAction): ObjectCacheState {
if (hasValue(state[action.payload])) {
let newObjectCache = Object.assign({}, state);
const newObjectCache = Object.assign({}, state);
delete newObjectCache[action.payload];
return newObjectCache;
}
else {
} else {
return state;
}
}
@@ -123,8 +122,8 @@ function removeFromObjectCache(state: ObjectCacheState, action: RemoveFromObject
* the new state, with all timeAdded timestamps set to the specified value
*/
function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObjectCacheTimestampsAction): ObjectCacheState {
let newState = Object.create(null);
Object.keys(state).forEach(key => {
const newState = Object.create(null);
Object.keys(state).forEach((key) => {
newState[key] = Object.assign({}, state[key], {
timeAdded: action.payload
});

View File

@@ -1,21 +1,22 @@
import { ObjectCacheState, CacheableObject } from "./object-cache.reducer";
import { Store } from "@ngrx/store";
import { ObjectCacheService } from "./object-cache.service";
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from "./object-cache.actions";
import { Observable } from "rxjs";
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ObjectCacheService } from './object-cache.service';
import { ObjectCacheState, CacheableObject } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
class TestClass implements CacheableObject {
constructor(
public uuid: string,
public foo: string
) {}
) { }
test(): string {
return this.foo + this.uuid;
}
}
describe("ObjectCacheService", () => {
describe('ObjectCacheService', () => {
let service: ObjectCacheService;
let store: Store<ObjectCacheState>;
@@ -39,76 +40,81 @@ describe("ObjectCacheService", () => {
spyOn(store, 'dispatch');
service = new ObjectCacheService(store);
spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
spyOn(Date.prototype, 'getTime').and.callFake(() => {
return timestamp;
});
});
describe("add", () => {
it("should dispatch an ADD action with the object to add, the time to live, and the current timestamp", () => {
describe('add', () => {
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
service.add(objectToCache, msToLive, requestHref);
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestHref));
});
});
describe("remove", () => {
it("should dispatch a REMOVE action with the UUID of the object to remove", () => {
describe('remove', () => {
it('should dispatch a REMOVE action with the UUID of the object to remove', () => {
service.remove(uuid);
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(uuid));
});
});
describe("get", () => {
it("should return an observable of the cached object with the specified UUID and type", () => {
describe('get', () => {
it('should return an observable of the cached object with the specified UUID and type', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
let testObj: any;
//due to the implementation of spyOn above, this subscribe will be synchronous
service.get(uuid, TestClass).take(1).subscribe(o => testObj = o);
// due to the implementation of spyOn above, this subscribe will be synchronous
service.get(uuid, TestClass).take(1).subscribe((o) => testObj = o);
expect(testObj.uuid).toBe(uuid);
expect(testObj.foo).toBe("bar");
expect(testObj.foo).toBe('bar');
// this only works if testObj is an instance of TestClass
expect(testObj.test()).toBe("bar" + uuid);
expect(testObj.test()).toBe('bar' + uuid);
});
it("should not return a cached object that has exceeded its time to live", () => {
it('should not return a cached object that has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
let getObsHasFired = false;
const subscription = service.get(uuid, TestClass).subscribe(o => getObsHasFired = true);
const subscription = service.get(uuid, TestClass).subscribe((o) => getObsHasFired = true);
expect(getObsHasFired).toBe(false);
subscription.unsubscribe();
});
});
describe("getList", () => {
it("should return an observable of the array of cached objects with the specified UUID and type", () => {
spyOn(service, 'get').and.returnValue(Observable.of(new TestClass(uuid, "bar")));
describe('getList', () => {
it('should return an observable of the array of cached objects with the specified UUID and type', () => {
spyOn(service, 'get').and.returnValue(Observable.of(new TestClass(uuid, 'bar')));
let testObjs: Array<any>;
service.getList([uuid, uuid], TestClass).take(1).subscribe(arr => testObjs = arr);
let testObjs: any[];
service.getList([uuid, uuid], TestClass).take(1).subscribe((arr) => testObjs = arr);
expect(testObjs[0].uuid).toBe(uuid);
expect(testObjs[0].foo).toBe("bar");
expect(testObjs[0].test()).toBe("bar" + uuid);
expect(testObjs[0].foo).toBe('bar');
expect(testObjs[0].test()).toBe('bar' + uuid);
expect(testObjs[1].uuid).toBe(uuid);
expect(testObjs[1].foo).toBe("bar");
expect(testObjs[1].test()).toBe("bar" + uuid);
expect(testObjs[1].foo).toBe('bar');
expect(testObjs[1].test()).toBe('bar' + uuid);
});
});
describe("has", () => {
it("should return true if the object with the supplied UUID is cached and still valid", () => {
describe('has', () => {
it('should return true if the object with the supplied UUID is cached and still valid', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
expect(service.has(uuid)).toBe(true);
});
it("should return false if the object with the supplied UUID isn't cached", () => {
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
expect(service.has(uuid)).toBe(false);
});
it("should return false if the object with the supplied UUID is cached but has exceeded its time to live", () => {
it('should return false if the object with the supplied UUID is cached but has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
expect(service.has(uuid)).toBe(false);
});
});
});

View File

@@ -1,10 +1,12 @@
import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import { ObjectCacheState, ObjectCacheEntry, CacheableObject } from "./object-cache.reducer";
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from "./object-cache.actions";
import { Observable } from "rxjs";
import { hasNoValue } from "../../shared/empty.util";
import { GenericConstructor } from "../shared/generic-constructor";
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ObjectCacheState, ObjectCacheEntry, CacheableObject } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { hasNoValue } from '../../shared/empty.util';
import { GenericConstructor } from '../shared/generic-constructor';
/**
* A service to interact with the object cache
@@ -13,7 +15,7 @@ import { GenericConstructor } from "../shared/generic-constructor";
export class ObjectCacheService {
constructor(
private store: Store<ObjectCacheState>
) {}
) { }
/**
* Add an object to the cache
@@ -59,7 +61,7 @@ export class ObjectCacheService {
*/
get<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
return this.getEntry(uuid)
.map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data));
.map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T);
}
getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> {
@@ -69,7 +71,7 @@ export class ObjectCacheService {
private getEntry(uuid: string): Observable<ObjectCacheEntry> {
return this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid)
.filter(entry => this.isValid(entry))
.filter((entry) => this.isValid(entry))
.distinctUntilChanged();
}
@@ -103,7 +105,7 @@ export class ObjectCacheService {
* The type of the objects to get
* @return Observable<Array<T>>
*/
getList<T extends CacheableObject>(uuids: Array<string>, type: GenericConstructor<T>): Observable<Array<T>> {
getList<T extends CacheableObject>(uuids: string[], type: GenericConstructor<T>): Observable<T[]> {
return Observable.combineLatest(
uuids.map((id: string) => this.get<T>(id, type))
);
@@ -123,7 +125,7 @@ export class ObjectCacheService {
this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid)
.take(1)
.subscribe(entry => result = this.isValid(entry));
.subscribe((entry) => result = this.isValid(entry));
return result;
}
@@ -138,7 +140,7 @@ export class ObjectCacheService {
* false otherwise
*/
hasBySelfLink(href: string): boolean {
let result: boolean = false;
let result = false;
this.store.select<string>('core', 'index', 'href', href)
.take(1)
@@ -159,8 +161,7 @@ export class ObjectCacheService {
private isValid(entry: ObjectCacheEntry): boolean {
if (hasNoValue(entry)) {
return false;
}
else {
} else {
const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) {

View File

@@ -1,6 +1,7 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { Response } from "./response-cache.models";
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { Response } from './response-cache.models';
/**
* The list of ResponseCacheAction type definitions
@@ -11,6 +12,7 @@ export const ResponseCacheActionTypes = {
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
};
/* tslint:disable:max-classes-per-file */
export class ResponseCacheAddAction implements Action {
type = ResponseCacheActionTypes.ADD;
payload: {
@@ -59,6 +61,7 @@ export class ResetResponseCacheTimestampsAction implements Action {
this.payload = newTimestamp;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all ResponseCacheActions

View File

@@ -1,16 +1,17 @@
import { RequestError } from "../data/request.models";
import { PageInfo } from "../shared/page-info.model";
import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model';
/* tslint:disable:max-classes-per-file */
export class Response {
constructor(
public isSuccessful: boolean,
public statusCode: string
) {}
) { }
}
export class SuccessResponse extends Response {
constructor(
public resourceUUIDs: Array<String>,
public resourceUUIDs: string[],
public statusCode: string,
public pageInfo?: PageInfo
) {
@@ -27,4 +28,4 @@ export class ErrorResponse extends Response {
this.errorMessage = error.message;
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -1,9 +1,11 @@
import { responseCacheReducer, ResponseCacheState } from "./response-cache.reducer";
import * as deepFreeze from 'deep-freeze';
import { responseCacheReducer, ResponseCacheState } from './response-cache.reducer';
import {
ResponseCacheRemoveAction,
ResetResponseCacheTimestampsAction
} from "./response-cache.actions";
import deepFreeze = require("deep-freeze");
} from './response-cache.actions';
class NullAction extends ResponseCacheRemoveAction {
type = null;
@@ -14,37 +16,37 @@ class NullAction extends ResponseCacheRemoveAction {
}
}
// describe("responseCacheReducer", () => {
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
// 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"
// '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 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
// '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
// 'key': keys[1],
// 'service': services[1],
// 'resourceID': resourceID,
// 'resourceUUIDs': [uuids[2]],
// 'isLoading': false,
// 'timeAdded': new Date().getTime(),
// 'msToLive': msToLive
// }
// };
// deepFreeze(testState);
@@ -57,29 +59,29 @@ class NullAction extends ResponseCacheRemoveAction {
// deepFreeze(errorState);
//
//
// it("should return the current state when no valid actions have been made", () => {
// 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", () => {
// 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", () => {
// describe('FIND_BY_ID', () => {
// const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID);
//
// it("should perform the action without affecting the previous state", () => {
// 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", () => {
// 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]);
@@ -87,28 +89,28 @@ class NullAction extends ResponseCacheRemoveAction {
// expect(newState[keys[0]].resourceID).toBe(resourceID);
// });
//
// it("should set responsePending to true", () => {
// 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", () => {
// 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", () => {
// describe('FIND_ALL', () => {
// const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
//
// it("should perform the action without affecting the previous state", () => {
// 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", () => {
// 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]);
@@ -118,84 +120,84 @@ class NullAction extends ResponseCacheRemoveAction {
// expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
// });
//
// it("should set responsePending to true", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// it('should remove any previous error message for the request', () => {
// const newState = responseCacheReducer(errorState, action);
// expect(newState[keys[0]].errorMessage).toBeUndefined();
// });
// });
//
// describe("ERROR", () => {
// describe('ERROR', () => {
// const errorMsg = 'errorMsg';
// const action = new ResponseCacheErrorAction(keys[0], errorMsg);
//
// it("should perform the action without affecting the previous state", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// 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", () => {
// 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";
// 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();
@@ -203,16 +205,16 @@ class NullAction extends ResponseCacheRemoveAction {
// });
// });
//
// describe("RESET_TIMESTAMPS", () => {
// describe('RESET_TIMESTAMPS', () => {
// const newTimeStamp = new Date().getTime();
// const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
//
// it("should perform the action without affecting the previous state", () => {
// 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", () => {
// 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);

View File

@@ -2,10 +2,10 @@ 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";
} 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
@@ -41,15 +41,15 @@ export const responseCacheReducer = (state = initialState, action: ResponseCache
switch (action.type) {
case ResponseCacheActionTypes.ADD: {
return addToCache(state, <ResponseCacheAddAction> action);
return addToCache(state, action as ResponseCacheAddAction);
}
case ResponseCacheActionTypes.REMOVE: {
return removeFromCache(state, <ResponseCacheRemoveAction> action);
return removeFromCache(state, action as ResponseCacheRemoveAction);
}
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
return resetResponseCacheTimestamps(state, <ResetResponseCacheTimestampsAction>action)
return resetResponseCacheTimestamps(state, action as ResetResponseCacheTimestampsAction)
}
default: {
@@ -81,12 +81,11 @@ function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction):
*/
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
if (hasValue(state[action.payload])) {
let newCache = Object.assign({}, state);
const newCache = Object.assign({}, state);
delete newCache[action.payload];
return newCache;
}
else {
} else {
return state;
}
}
@@ -102,8 +101,8 @@ function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveA
* 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 => {
const newState = Object.create(null);
Object.keys(state).forEach((key) => {
newState[key] = Object.assign({}, state[key], {
timeAdded: action.payload
});

View File

@@ -1,18 +1,19 @@
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";
import { OpaqueToken } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
// describe("ResponseCacheService", () => {
import { ResponseCacheService } from './response-cache.service';
import { ResponseCacheState, ResponseCacheEntry } from './response-cache.reducer';
// describe('ResponseCacheService', () => {
// let service: ResponseCacheService;
// let store: Store<ResponseCacheState>;
//
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
// 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 resourceID = '9978';
// const paginationOptions = { 'resultsPerPage': 10, 'currentPage': 1 };
// const sortOptions = { 'field': 'id', 'direction': 0 };
// const timestamp = new Date().getTime();
// const validCacheEntry = (key) => {
// return {
@@ -36,33 +37,33 @@ import { Observable } from "rxjs";
// spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
// });
//
// describe("findAll", () => {
// describe('findAll', () => {
// beforeEach(() => {
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
// spyOn(service, 'get').and.callFake((key) => Observable.of({key: key}));
// });
// describe("if the key isn't cached", () => {
// describe('if the key isn't cached', () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(false);
// spyOn(service, 'has').and.returnValue(false);
// });
// it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
// 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", () => {
// 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", () => {
// describe('if the key is already cached', () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(true);
// spyOn(service, 'has').and.returnValue(true);
// });
// it("shouldn't dispatch anything", () => {
// 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", () => {
// 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]);
@@ -70,33 +71,33 @@ import { Observable } from "rxjs";
// });
// });
//
// describe("findById", () => {
// describe('findById', () => {
// beforeEach(() => {
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
// spyOn(service, 'get').and.callFake((key) => Observable.of({key: key}));
// });
// describe("if the key isn't cached", () => {
// describe('if the key isn't cached', () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(false);
// spyOn(service, 'has').and.returnValue(false);
// });
// it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
// 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", () => {
// 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", () => {
// describe('if the key is already cached', () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(true);
// spyOn(service, 'has').and.returnValue(true);
// });
// it("shouldn't dispatch anything", () => {
// 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", () => {
// 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]);
@@ -104,9 +105,9 @@ import { Observable } from "rxjs";
// });
// });
//
// describe("get", () => {
// it("should return an observable of the cached request with the specified key", () => {
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
// describe('get', () => {
// it('should return an observable of the cached request with the specified key', () => {
// spyOn(store, 'select').and.callFake((...args:Array<any>) => {
// return Observable.of(validCacheEntry(args[args.length - 1]));
// });
//
@@ -115,8 +116,8 @@ import { Observable } from "rxjs";
// expect(testObj.key).toEqual(keys[1]);
// });
//
// it("should not return a cached request that has exceeded its time to live", () => {
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
// it('should not return a cached request that has exceeded its time to live', () => {
// spyOn(store, 'select').and.callFake((...args:Array<any>) => {
// return Observable.of(invalidCacheEntry(args[args.length - 1]));
// });
//
@@ -127,18 +128,18 @@ import { Observable } from "rxjs";
// });
// });
//
// describe("has", () => {
// it("should return true if the request with the supplied key is cached and still valid", () => {
// 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", () => {
// 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", () => {
// 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);
// });

View File

@@ -1,15 +1,12 @@
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";
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheState, ResponseCacheEntry } from './response-cache.reducer';
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
@@ -18,13 +15,13 @@ import { Response } from "./response-cache.models";
export class ResponseCacheService {
constructor(
private store: Store<ResponseCacheState>
) {}
) { }
add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> {
if (!this.has(key)) {
// this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
}
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);
}
@@ -38,7 +35,7 @@ export class ResponseCacheService {
*/
get(key: string): Observable<ResponseCacheEntry> {
return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
.filter(entry => this.isValid(entry))
.filter((entry) => this.isValid(entry))
.distinctUntilChanged()
}
@@ -56,7 +53,7 @@ export class ResponseCacheService {
this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
.take(1)
.subscribe(entry => {
.subscribe((entry) => {
result = this.isValid(entry);
});
@@ -75,8 +72,7 @@ export class ResponseCacheService {
private isValid(entry: ResponseCacheEntry): boolean {
if (hasNoValue(entry)) {
return false;
}
else {
} else {
const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) {

View File

@@ -1,8 +1,9 @@
import { EffectsModule } from "@ngrx/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";
import { EffectsModule } from '@ngrx/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(RequestEffects),

View File

@@ -1,18 +1,19 @@
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 { 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 { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
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 { 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 { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
const IMPORTS = [
CommonModule,
@@ -40,18 +41,12 @@ const PROVIDERS = [
];
@NgModule({
imports: [ ...IMPORTS ],
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 {
@@ -61,4 +56,11 @@ export class CoreModule {
]
};
}
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
if (isNotEmpty(parentModule)) {
throw new Error('CoreModule is already loaded. Import it in the AppModule only');
}
}
}

View File

@@ -1,7 +1,8 @@
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";
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,

View File

@@ -1,14 +1,15 @@
import { Inject, 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";
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
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 { 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';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable()
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
@@ -23,7 +24,7 @@ export class CollectionDataService extends DataService<NormalizedCollection, Col
protected store: Store<CoreState>,
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
) {
super(NormalizedCollection, EnvConfig);
super(NormalizedCollection, EnvConfig);
}
}

View File

@@ -1,14 +1,16 @@
import { Inject, 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";
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
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 { 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';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable()
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {

View File

@@ -1,5 +1,6 @@
import { combineReducers } from "@ngrx/store";
import { RequestState, requestReducer } from "./request.reducer";
import { combineReducers } from '@ngrx/store';
import { RequestState, requestReducer } from './request.reducer';
export interface DataState {
request: RequestState

View File

@@ -1,18 +1,18 @@
import { ObjectCacheService } from "../cache/object-cache.service";
import { ResponseCacheService } from "../cache/response-cache.service";
import { CacheableObject } from "../cache/object-cache.reducer";
import { hasValue, isNotEmpty } from "../../shared/empty.util";
import { RemoteData } from "./remote-data";
import { FindAllOptions, 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";
import { Inject } from "@angular/core";
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CacheableObject } from '../cache/object-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from './remote-data';
import { FindAllOptions, 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';
import { Inject } from '@angular/core';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
protected abstract objectCache: ObjectCacheService;
@@ -32,17 +32,16 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
protected getFindAllHref(options: FindAllOptions = {}): string {
let result;
let args = [];
const args = [];
if (hasValue(options.scopeID)) {
result = this.browseEndpoint;
args.push(`scope=${options.scopeID}`);
}
else {
} else {
result = this.resourceEndpoint;
}
if (hasValue(options.currentPage) && typeof options.currentPage === "number") {
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
args.push(`page=${options.currentPage - 1}`);
}
@@ -65,7 +64,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
return new RESTURLCombiner(this.EnvConfig, result).toString();
}
findAll(options: FindAllOptions = {}): RemoteData<Array<TDomain>> {
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
const href = this.getFindAllHref(options);
const request = new FindAllRequest(href, options);
this.requestService.configure(request);

View File

@@ -1,14 +1,16 @@
import { Inject, 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";
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
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 { 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';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -22,7 +24,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
) {
) {
super(NormalizedItem, EnvConfig);
}
}

View File

@@ -1,18 +1,15 @@
import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { StoreActionTypes } from "../../store.actions";
import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions";
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { StoreActionTypes } from '../../store.actions';
import { ResetObjectCacheTimestampsAction } from '../cache/object-cache.actions';
@Injectable()
export class ObjectCacheEffects {
constructor(
private actions$: Actions
) { }
/**
* When the store is rehydrated in the browser, set all cache
* timestamps to "now", because the time zone of the server can
* timestamps to 'now', because the time zone of the server can
* differ from the client.
*
* This assumes that the server cached everything a negligible
@@ -22,4 +19,6 @@ export class ObjectCacheEffects {
.ofType(StoreActionTypes.REHYDRATE)
.map(() => new ResetObjectCacheTimestampsAction(new Date().getTime()));
constructor(private actions$: Actions) { }
}

View File

@@ -1,11 +1,12 @@
import { Observable } from "rxjs";
import { PageInfo } from "../shared/page-info.model";
import { Observable } from 'rxjs/Observable';
import { PageInfo } from '../shared/page-info.model';
export enum RemoteDataState {
RequestPending = <any> "RequestPending",
ResponsePending = <any> "ResponsePending",
Failed = <any> "Failed",
Success = <any> "Success"
RequestPending = 'RequestPending' as any,
ResponsePending = 'ResponsePending' as any,
Failed = 'Failed' as any,
Success = 'Success' as any
}
/**
@@ -32,14 +33,11 @@ export class RemoteData<T> {
(requestPending, responsePending, isSuccessFul) => {
if (requestPending) {
return RemoteDataState.RequestPending
}
else if (responsePending) {
} else if (responsePending) {
return RemoteDataState.ResponsePending
}
else if (!isSuccessFul) {
} else if (!isSuccessFul) {
return RemoteDataState.Failed
}
else {
} else {
return RemoteDataState.Success
}
}
@@ -47,36 +45,26 @@ export class RemoteData<T> {
}
get isRequestPending(): Observable<boolean> {
return this.state
.map(state => state == RemoteDataState.RequestPending)
.distinctUntilChanged();
return this.state.map((state) => state === RemoteDataState.RequestPending).distinctUntilChanged();
}
get isResponsePending(): Observable<boolean> {
return this.state
.map(state => state == RemoteDataState.ResponsePending)
.distinctUntilChanged();
return this.state.map((state) => state === RemoteDataState.ResponsePending).distinctUntilChanged();
}
get isLoading(): Observable<boolean> {
return this.state
.map(state => {
return state == RemoteDataState.RequestPending
|| state === RemoteDataState.ResponsePending
})
.distinctUntilChanged();
return this.state.map((state) => {
return state === RemoteDataState.RequestPending
|| state === RemoteDataState.ResponsePending
}).distinctUntilChanged();
}
get hasFailed(): Observable<boolean> {
return this.state
.map(state => state == RemoteDataState.Failed)
.distinctUntilChanged();
return this.state.map((state) => state === RemoteDataState.Failed).distinctUntilChanged();
}
get hasSucceeded(): Observable<boolean> {
return this.state
.map(state => state == RemoteDataState.Success)
.distinctUntilChanged();
return this.state.map((state) => state === RemoteDataState.Success).distinctUntilChanged();
}
}

View File

@@ -1,20 +1,15 @@
import { Injectable, Inject } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { ObjectCacheActionTypes } from "../cache/object-cache.actions";
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
import { ResetResponseCacheTimestampsAction } from "../cache/response-cache.actions";
import { Injectable, Inject } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { ObjectCacheActionTypes } from '../cache/object-cache.actions';
import { ResetResponseCacheTimestampsAction } from '../cache/response-cache.actions';
@Injectable()
export class RequestCacheEffects {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions,
) { }
/**
* When the store is rehydrated in the browser, set all cache
* timestamps to "now", because the time zone of the server can
* timestamps to 'now', because the time zone of the server can
* differ from the client.
*
* This assumes that the server cached everything a negligible
@@ -31,4 +26,7 @@ export class RequestCacheEffects {
@Effect() fixTimestampsOnRehydrate = this.actions$
.ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
constructor(private actions$: Actions, ) { }
}

View File

@@ -1,7 +1,7 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { CacheableObject } from "../cache/object-cache.reducer";
import { Request } from "./request.models";
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
@@ -12,6 +12,7 @@ export const RequestActionTypes = {
COMPLETE: type('dspace/core/data/request/COMPLETE')
};
/* tslint:disable:max-classes-per-file */
export class RequestConfigureAction implements Action {
type = RequestActionTypes.CONFIGURE;
payload: Request<CacheableObject>;
@@ -49,6 +50,7 @@ export class RequestCompleteAction implements Action {
this.payload = key;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all RequestActions

View File

@@ -1,26 +1,27 @@
import { Injectable, Inject } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
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, hasValue, isEmpty, isNotEmpty } from "../../shared/empty.util";
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
import { RequestEntry } from "./request.reducer";
import {
RequestActionTypes, RequestExecuteAction,
RequestCompleteAction
} from "./request.actions";
import { ResponseCacheService } from "../cache/response-cache.service";
import { RequestService } from "./request.service";
import { NormalizedObjectFactory } from "../cache/models/normalized-object-factory";
import { ResourceType } from "../shared/resource-type";
import { RequestError } from "./request.models";
import { PageInfo } from "../shared/page-info.model";
import { NormalizedObject } from "../cache/models/normalized-object.model";
import { Injectable, Inject } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
// tslint:disable-next-line:import-blacklist
import { Observable } from 'rxjs';
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 { Response, SuccessResponse, ErrorResponse } from '../cache/response-cache.models';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RequestEntry } from './request.reducer';
import { RequestActionTypes, RequestExecuteAction, RequestCompleteAction } from './request.actions';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from './request.service';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { ResourceType } from '../shared/resource-type';
import { RequestError } from './request.models';
import { PageInfo } from '../shared/page-info.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { GlobalConfig, GLOBAL_CONFIG } from '../../../config';
function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
@@ -38,23 +39,14 @@ function flattenSingleKeyObject(obj: any): any {
return obj[keys[0]];
}
/* tslint:disable:max-classes-per-file */
class ProcessRequestDTO {
[key: string]: NormalizedObject[]
}
@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
) { }
@Effect() execute = this.actions$
.ofType(RequestActionTypes.EXECUTE)
.flatMap((action: RequestExecuteAction) => {
@@ -65,48 +57,52 @@ export class RequestEffects {
return this.restApi.get(entry.request.href)
.map((data: DSpaceRESTV2Response) => {
const processRequestDTO = this.process(data.payload, entry.request.href);
const uuids = flattenSingleKeyObject(processRequestDTO).map(no => no.uuid);
const uuids = flattenSingleKeyObject(processRequestDTO).map((no) => no.uuid);
return new SuccessResponse(uuids, data.statusCode, this.processPageInfo(data.payload.page))
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href))
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
});
protected process(data: any, requestHref: string): ProcessRequestDTO {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private objectCache: ObjectCacheService,
private responseCache: ResponseCacheService,
protected requestService: RequestService
) { }
protected process(data: any, requestHref: string): ProcessRequestDTO {
if (isNotEmpty(data)) {
if (isPaginatedResponse(data)) {
return this.process(data._embedded, requestHref);
}
else if (isObjectLevel(data)) {
return { "topLevel": this.deserializeAndCache(data, requestHref) };
}
else {
let result = new ProcessRequestDTO();
} else if (isObjectLevel(data)) {
return { topLevel: this.deserializeAndCache(data, requestHref) };
} else {
const result = new ProcessRequestDTO();
if (Array.isArray(data)) {
result['topLevel'] = [];
data.forEach(datum => {
result.topLevel = [];
data.forEach((datum) => {
if (isPaginatedResponse(datum)) {
const obj = this.process(datum, requestHref);
result['topLevel'] = [...result['topLevel'], ...flattenSingleKeyObject(obj)];
}
else {
result['topLevel'] = [...result['topLevel'], ...this.deserializeAndCache(datum, requestHref)];
result.topLevel = [...result.topLevel, ...flattenSingleKeyObject(obj)];
} else {
result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)];
}
});
}
else {
} else {
Object.keys(data)
.filter(property => data.hasOwnProperty(property))
.filter(property => hasValue(data[property]))
.forEach(property => {
.filter((property) => data.hasOwnProperty(property))
.filter((property) => hasValue(data[property]))
.forEach((property) => {
if (isPaginatedResponse(data[property])) {
const obj = this.process(data[property], requestHref);
result[property] = flattenSingleKeyObject(obj);
}
else {
} else {
result[property] = this.deserializeAndCache(data[property], requestHref);
}
});
@@ -117,13 +113,13 @@ export class RequestEffects {
}
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
if(Array.isArray(obj)) {
if (Array.isArray(obj)) {
let result = [];
obj.forEach(o => result = [...result, ...this.deserializeAndCache(o, requestHref)])
obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)])
return result;
}
let type: ResourceType = obj["type"];
const type: ResourceType = obj.type;
if (hasValue(type)) {
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
@@ -134,11 +130,11 @@ export class RequestEffects {
if (isNotEmpty(obj._embedded)) {
processed = this.process(obj._embedded, requestHref);
}
let normalizedObj = serializer.deserialize(obj);
const normalizedObj = serializer.deserialize(obj);
if (isNotEmpty(processed)) {
let linksOnly = {};
Object.keys(processed).forEach(key => {
const linksOnly = {};
Object.keys(processed).forEach((key) => {
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
});
Object.assign(normalizedObj, linksOnly);
@@ -147,16 +143,14 @@ export class RequestEffects {
this.addToObjectCache(normalizedObj, requestHref);
return [normalizedObj];
}
else {
//TODO move check to Validator?
} else {
// TODO: move check to Validator?
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
return [];
}
}
else {
//TODO move check to Validator
} else {
// TODO: move check to Validator
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
return [];
}
@@ -172,10 +166,10 @@ export class RequestEffects {
protected processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
}
else {
} else {
return undefined;
}
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -1,11 +1,12 @@
import { SortOptions } from "../cache/models/sort-options.model";
import { PaginationComponentOptions } from "../../shared/pagination/pagination-component-options.model";
import { GenericConstructor } from "../shared/generic-constructor";
import { SortOptions } from '../cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { GenericConstructor } from '../shared/generic-constructor';
/* tslint:disable:max-classes-per-file */
export class Request<T> {
constructor(
public href: string,
) {}
) { }
}
export class FindByIDRequest<T> extends Request<T> {
@@ -36,3 +37,4 @@ export class FindAllRequest<T> extends Request<T> {
export class RequestError extends Error {
statusText: string;
}
/* tslint:enable:max-classes-per-file */

View File

@@ -1,9 +1,9 @@
import { CacheableObject } from "../cache/object-cache.reducer";
import { CacheableObject } from '../cache/object-cache.reducer';
import {
RequestActionTypes, RequestAction, RequestConfigureAction,
RequestExecuteAction, RequestCompleteAction
} from "./request.actions";
import { Request } from "./request.models";
} from './request.actions';
import { Request } from './request.models';
export class RequestEntry {
request: Request<CacheableObject>;
@@ -12,7 +12,6 @@ export class RequestEntry {
completed: boolean;
}
export interface RequestState {
[key: string]: RequestEntry
}
@@ -24,15 +23,15 @@ export const requestReducer = (state = initialState, action: RequestAction): Req
switch (action.type) {
case RequestActionTypes.CONFIGURE: {
return configureRequest(state, <RequestConfigureAction> action);
return configureRequest(state, action as RequestConfigureAction);
}
case RequestActionTypes.EXECUTE: {
return executeRequest(state, <RequestExecuteAction> action);
return executeRequest(state, action as RequestExecuteAction);
}
case RequestActionTypes.COMPLETE: {
return completeRequest(state, <RequestCompleteAction> action);
return completeRequest(state, action as RequestCompleteAction);
}
default: {

View File

@@ -1,16 +1,20 @@
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 { ResponseCacheEntry } from "../cache/response-cache.reducer";
import { request } from "http";
import { SuccessResponse } from "../cache/response-cache.models";
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { request } from 'http';
import { Observable } from 'rxjs/Observable';
import { RequestEntry, RequestState } from './request.reducer';
import { Request } from './request.models';
import { hasValue } from '../../shared/empty.util';
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 { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { SuccessResponse } from '../cache/response-cache.models';
@Injectable()
export class RequestService {
@@ -26,9 +30,9 @@ export class RequestService {
let isPending = false;
this.store.select<RequestEntry>('core', 'data', 'request', href)
.take(1)
.subscribe((re: RequestEntry) => {
.subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !re.completed)
});
});
return isPending;
}
@@ -41,14 +45,14 @@ export class RequestService {
let isCached = this.objectCache.hasBySelfLink(request.href);
if (!isCached && this.responseCache.has(request.href)) {
//if it isn't cached it may be a list endpoint, if so verify
//every object included in the response is still cached
// if it isn't cached it may be a list endpoint, if so verify
// every object included in the response is still cached
this.responseCache.get(request.href)
.take(1)
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.map((resourceUUIDs: Array<string>) => resourceUUIDs.every(uuid => this.objectCache.has(uuid)))
.subscribe(c => isCached = c);
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs)
.map((resourceUUIDs: string[]) => resourceUUIDs.every((uuid) => this.objectCache.has(uuid)))
.subscribe((c) => isCached = c);
}
const isPending = this.isPending(request.href);

View File

@@ -16,8 +16,7 @@
"description": "Object of links with the rels as the keys",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"oneOf": [{
"$ref": "#/definitions/linkObject"
},
{
@@ -43,8 +42,7 @@
"$ref": "http://hyperschema.org/core/base#/definitions/name"
},
"href": {
"anyOf": [
{
"anyOf": [{
"$ref": "http://hyperschema.org/core/link#/definitions/href"
},
{
@@ -71,8 +69,7 @@
"description": "An embedded HAL resource",
"type": "object",
"additionalProperties": {
"oneOf": [
{
"oneOf": [{
"$ref": "#"
},
{

View File

@@ -1,5 +1,6 @@
import { DSpaceRESTv2Serializer } from "./dspace-rest-v2.serializer";
import { autoserialize, autoserializeAs } from "cerialize";
import { autoserialize, autoserializeAs } from 'cerialize';
import { DSpaceRESTv2Serializer } from './dspace-rest-v2.serializer';
class TestModel {
@autoserialize
@@ -9,55 +10,54 @@ class TestModel {
name: string;
@autoserializeAs(TestModel)
parents?: Array<TestModel>;
parents?: TestModel[];
}
const testModels = [
{
"id": "d4466d54-d73b-4d8f-b73f-c702020baa14",
"name": "Model 1",
id: 'd4466d54-d73b-4d8f-b73f-c702020baa14',
name: 'Model 1',
},
{
"id": "752a1250-949a-46ad-9bea-fbc45f0b656d",
"name": "Model 2",
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" }
_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"
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" }
_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"
id: '598ce822-c357-46f3-ab70-63724d02d6ad',
type: 'testModels',
name: 'Another Test Model'
}
];
const parentHrefRegex = /^\/testmodels\/(.+)$/g;
describe('DSpaceRESTv2Serializer', () => {
describe("DSpaceRESTv2Serializer", () => {
describe('serialize', () => {
describe("serialize", () => {
it("should turn a model in to a valid document", () => {
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.id);
@@ -66,9 +66,9 @@ describe("DSpaceRESTv2Serializer", () => {
});
describe("serializeArray", () => {
describe('serializeArray', () => {
it("should turn an array of models in to a valid document", () => {
it('should turn an array of models in to a valid document', () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = serializer.serializeArray(testModels);
@@ -80,9 +80,9 @@ describe("DSpaceRESTv2Serializer", () => {
});
describe("deserialize", () => {
describe('deserialize', () => {
it("should turn a valid document describing a single entity in to a valid model", () => {
it('should turn a valid document describing a single entity in to a valid model', () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const model = serializer.deserialize(testResponses[0]);
@@ -90,12 +90,12 @@ describe("DSpaceRESTv2Serializer", () => {
expect(model.name).toBe(testResponses[0].name);
});
//TODO cant implement/test this yet - depends on how relationships
// TODO: cant implement/test this yet - depends on how relationships
// will be handled in the rest api
// it("should retain relationship information", () => {
// it('should retain relationship information', () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = {
// "_embedded": testResponses[0],
// '_embedded': testResponses[0],
// };
//
// const model = serializer.deserialize(doc);
@@ -112,7 +112,7 @@ describe("DSpaceRESTv2Serializer", () => {
// });
// TODO enable once validation is enabled in the serializer
// it("should throw an error when dealing with an invalid document", () => {
// it('should throw an error when dealing with an invalid document', () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = testResponses[0];
//
@@ -121,7 +121,7 @@ describe("DSpaceRESTv2Serializer", () => {
// }).toThrow();
// });
it("should throw an error when dealing with a document describing an array", () => {
it('should throw an error when dealing with a document describing an array', () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
expect(() => {
serializer.deserialize(testResponses);
@@ -130,13 +130,13 @@ describe("DSpaceRESTv2Serializer", () => {
});
describe("deserializeArray", () => {
describe('deserializeArray', () => {
//TODO rewrite to incorporate normalisation.
// it("should turn a valid document describing a collection of objects in to an array of valid models", () => {
// 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
// '_embedded': testResponses
// };
//
// const models = serializer.deserializeArray(doc);
@@ -147,12 +147,12 @@ describe("DSpaceRESTv2Serializer", () => {
// expect(models[1].name).toBe(doc._embedded[1].name);
// });
//TODO cant implement/test this yet - depends on how relationships
// TODO: cant implement/test this yet - depends on how relationships
// will be handled in the rest api
// it("should retain relationship information", () => {
// it('should retain relationship information', () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = {
// "_embedded": testResponses,
// '_embedded': testResponses,
// };
//
// const models = serializer.deserializeArray(doc);
@@ -169,7 +169,7 @@ describe("DSpaceRESTv2Serializer", () => {
// });
// TODO enable once validation is enabled in the serializer
// it("should throw an error when dealing with an invalid document", () => {
// it('should throw an error when dealing with an invalid document', () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = testResponses[0];
//
@@ -178,10 +178,10 @@ describe("DSpaceRESTv2Serializer", () => {
// }).toThrow();
// });
it("should throw an error when dealing with a document describing a single model", () => {
it('should throw an error when dealing with a document describing a single model', () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = {
"_embedded": testResponses[0]
_embedded: testResponses[0]
};
expect(() => {

View File

@@ -1,9 +1,10 @@
import { Serialize, Deserialize } from "cerialize";
import { Serializer } from "../serializer";
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
import { DSpaceRESTv2Validator } from "./dspace-rest-v2.validator";
import { GenericConstructor } from "../shared/generic-constructor";
import { hasNoValue, hasValue } from "../../shared/empty.util";
import { Serialize, Deserialize } from 'cerialize';
import { Serializer } from '../serializer';
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
import { DSpaceRESTv2Validator } from './dspace-rest-v2.validator';
import { GenericConstructor } from '../shared/generic-constructor';
import { hasNoValue, hasValue } from '../../shared/empty.util';
/**
* This Serializer turns responses from v2 of DSpace's REST API
@@ -36,8 +37,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: Array<T>): any {
return Serialize(models, this.modelType);
serializeArray(models: T[]): any {
return Serialize(models, this.modelType);
}
/**
@@ -52,8 +53,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
if (Array.isArray(response)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
let normalized = Object.assign({}, response, this.normalizeLinks(response._links));
return <T> Deserialize(normalized, this.modelType);
const normalized = Object.assign({}, response, this.normalizeLinks(response._links));
return Deserialize(normalized, this.modelType) as T;
}
/**
@@ -62,28 +63,27 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: any): Array<T> {
//TODO enable validation, once rest data stabilizes
deserializeArray(response: any): T[] {
// TODO: enable validation, once rest data stabilizes
// new DSpaceRESTv2Validator(response).validate();
if (!Array.isArray(response)) {
throw new Error('Expected an Array, use deserialize() instead');
}
let normalized = response.map((resource) => {
return Object.assign({}, resource, this.normalizeLinks(resource._links));
const normalized = response.map((resource) => {
return Object.assign({}, resource, this.normalizeLinks(resource._links));
});
return <Array<T>> Deserialize(normalized, this.modelType);
return Deserialize(normalized, this.modelType) as T[];
}
private normalizeLinks(links:any): any {
let normalizedLinks = links;
for (let link in normalizedLinks) {
private normalizeLinks(links: any): any {
const normalizedLinks = links;
for (const link in normalizedLinks) {
if (Array.isArray(normalizedLinks[link])) {
normalizedLinks[link] = normalizedLinks[link].map(linkedResource => {
normalizedLinks[link] = normalizedLinks[link].map((linkedResource) => {
return linkedResource.href;
});
}
else {
} else {
normalizedLinks[link] = normalizedLinks[link].href;
}
}

View File

@@ -1,16 +1,18 @@
import { Inject, Injectable } from '@angular/core';
import { Http, RequestOptionsArgs } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
/**
* Service to access DSpace's REST API
*/
@Injectable()
export class DSpaceRESTv2Service {
constructor(private http: Http, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
}
@@ -27,8 +29,8 @@ export class DSpaceRESTv2Service {
*/
get(absoluteURL: string, options?: RequestOptionsArgs): Observable<DSpaceRESTV2Response> {
return this.http.get(absoluteURL, options)
.map(res => ({ payload: res.json(), statusCode: res.statusText }))
.catch(err => {
.map((res) => ({ payload: res.json(), statusCode: res.statusText }))
.catch((err) => {
console.log('Error: ', err);
return Observable.throw(err);
});

View File

@@ -1,5 +1,6 @@
import * as schema from './dspace-rest-v2.schema.json'
import { Validator } from "jsonschema";
import { Validator } from 'jsonschema';
import schema from './dspace-rest-v2.schema.json'
/**
* Verifies a document is a valid response from
@@ -22,11 +23,10 @@ export class DSpaceRESTv2Validator {
if (result.errors && result.errors.length > 0) {
const message = result.errors
.map((error) => error.message)
.join("\n");
.join('\n');
throw new Error(message);
}
else {
throw new Error("JSON API validation failed for an unknown reason");
} else {
throw new Error('JSON API validation failed for an unknown reason');
}
}
}

View File

@@ -1,14 +1,13 @@
@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;
border-top: $footer-border;
text-align: center;
padding: $footer-padding;
p {

View File

@@ -5,19 +5,23 @@ import {
inject,
TestBed
} from '@angular/core/testing';
import {
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from "@angular/core";
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { By } from '@angular/platform-browser';
import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
import { Store, StoreModule } from "@ngrx/store";
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
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";
import { MockTranslateLoader } from '../../shared/testing/mock-translate-loader';
let comp: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;

View File

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

View File

@@ -1,5 +1,6 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
/**
* The list of HrefIndexAction type definitions
@@ -9,6 +10,7 @@ export const HrefIndexActionTypes = {
REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add an href to the index
*/
@@ -48,11 +50,11 @@ export class RemoveUUIDFromHrefIndexAction implements Action {
constructor(uuid: string) {
this.payload = uuid;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all HrefIndexActions
*/
export type HrefIndexAction
= AddToHrefIndexAction
| RemoveUUIDFromHrefIndexAction;
export type HrefIndexAction = AddToHrefIndexAction | RemoveUUIDFromHrefIndexAction;

View File

@@ -1,19 +1,16 @@
import { Injectable } from "@angular/core";
import { Effect, Actions } from "@ngrx/effects";
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";
} 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))
@@ -29,4 +26,9 @@ export class HrefIndexEffects {
.map((action: RemoveFromObjectCacheAction) => {
return new RemoveUUIDFromHrefIndexAction(action.payload);
});
constructor(private actions$: Actions) {
}
}

View File

@@ -1,7 +1,10 @@
import {
HrefIndexAction, HrefIndexActionTypes, AddToHrefIndexAction,
HrefIndexAction,
HrefIndexActionTypes,
AddToHrefIndexAction,
RemoveUUIDFromHrefIndexAction
} from "./href-index.actions";
} from './href-index.actions';
export interface HrefIndexState {
[href: string]: string
}
@@ -13,11 +16,11 @@ export const hrefIndexReducer = (state = initialState, action: HrefIndexAction):
switch (action.type) {
case HrefIndexActionTypes.ADD: {
return addToHrefIndex(state, <AddToHrefIndexAction>action);
return addToHrefIndex(state, action as AddToHrefIndexAction);
}
case HrefIndexActionTypes.REMOVE_UUID: {
return removeUUIDFromHrefIndex(state, <RemoveUUIDFromHrefIndexAction>action)
return removeUUIDFromHrefIndex(state, action as RemoveUUIDFromHrefIndexAction)
}
default: {
@@ -33,8 +36,8 @@ function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): Hr
}
function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
let newState = Object.create(null);
for (let href in state) {
const newState = Object.create(null);
for (const href in state) {
if (state[href] !== action.payload) {
newState[href] = state[href];
}

View File

@@ -1,5 +1,6 @@
import { combineReducers } from "@ngrx/store";
import { HrefIndexState, hrefIndexReducer } from "./href-index.reducer";
import { combineReducers } from '@ngrx/store';
import { HrefIndexState, hrefIndexReducer } from './href-index.reducer';
export interface IndexState {
href: HrefIndexState

View File

@@ -18,7 +18,7 @@ export interface Serializer<T> {
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: Array<T>): any;
serializeArray(models: T[]): any;
/**
* Convert a response from the backend in to a model.
@@ -34,5 +34,5 @@ export interface Serializer<T> {
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: any): Array<T>;
deserializeArray(response: any): T[];
}

View File

@@ -1,6 +1,6 @@
import { DSpaceObject } from "./dspace-object.model";
import { RemoteData } from "../data/remote-data";
import { Item } from "./item.model";
import { DSpaceObject } from './dspace-object.model';
import { RemoteData } from '../data/remote-data';
import { Item } from './item.model';
export class Bitstream extends DSpaceObject {

View File

@@ -1,7 +1,7 @@
import { DSpaceObject } from "./dspace-object.model";
import { Bitstream } from "./bitstream.model";
import { Item } from "./item.model";
import { RemoteData } from "../data/remote-data";
import { DSpaceObject } from './dspace-object.model';
import { Bitstream } from './bitstream.model';
import { Item } from './item.model';
import { RemoteData } from '../data/remote-data';
export class Bundle extends DSpaceObject {
/**

View File

@@ -1,7 +1,7 @@
import { DSpaceObject } from "./dspace-object.model";
import { Bitstream } from "./bitstream.model";
import { Item } from "./item.model";
import { RemoteData } from "../data/remote-data";
import { DSpaceObject } from './dspace-object.model';
import { Bitstream } from './bitstream.model';
import { Item } from './item.model';
import { RemoteData } from '../data/remote-data';
export class Collection extends DSpaceObject {
@@ -15,7 +15,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description
*/
get introductoryText(): string {
return this.findMetadata("dc.description");
return this.findMetadata('dc.description');
}
/**
@@ -23,7 +23,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description.abstract
*/
get shortDescription(): string {
return this.findMetadata("dc.description.abstract");
return this.findMetadata('dc.description.abstract');
}
/**
@@ -31,7 +31,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.rights
*/
get copyrightText(): string {
return this.findMetadata("dc.rights");
return this.findMetadata('dc.rights');
}
/**
@@ -39,7 +39,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.rights.license
*/
get license(): string {
return this.findMetadata("dc.rights.license");
return this.findMetadata('dc.rights.license');
}
/**
@@ -47,7 +47,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description.tableofcontents
*/
get sidebarText(): string {
return this.findMetadata("dc.description.tableofcontents");
return this.findMetadata('dc.description.tableofcontents');
}
/**

View File

@@ -1,7 +1,7 @@
import { DSpaceObject } from "./dspace-object.model";
import { Bitstream } from "./bitstream.model";
import { Collection } from "./collection.model";
import { RemoteData } from "../data/remote-data";
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 {
@@ -15,7 +15,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description
*/
get introductoryText(): string {
return this.findMetadata("dc.description");
return this.findMetadata('dc.description');
}
/**
@@ -23,7 +23,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description.abstract
*/
get shortDescription(): string {
return this.findMetadata("dc.description.abstract");
return this.findMetadata('dc.description.abstract');
}
/**
@@ -31,7 +31,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.rights
*/
get copyrightText(): string {
return this.findMetadata("dc.rights");
return this.findMetadata('dc.rights');
}
/**
@@ -39,7 +39,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description.tableofcontents
*/
get sidebarText(): string {
return this.findMetadata("dc.description.tableofcontents");
return this.findMetadata('dc.description.tableofcontents');
}
/**

View File

@@ -1,90 +1,87 @@
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";
import { ResourceType } from "./resource-type";
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';
import { ResourceType } from './resource-type';
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class DSpaceObject implements CacheableObject {
self: string;
self: string;
/**
* The human-readable identifier of this DSpaceObject
*/
id: string;
/**
* The human-readable identifier of this DSpaceObject
*/
id: string;
/**
* The universally unique identifier of this DSpaceObject
*/
uuid: string;
/**
* The universally unique identifier of this DSpaceObject
*/
uuid: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
type: ResourceType;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
type: ResourceType;
/**
* The name for this DSpaceObject
*/
name: string;
/**
* The name for this DSpaceObject
*/
name: string;
/**
* An array containing all metadata of this DSpaceObject
*/
metadata: Array<Metadatum>;
/**
* An array containing all metadata of this DSpaceObject
*/
metadata: Metadatum[];
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
parents: RemoteData<DSpaceObject[]>;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
parents: RemoteData<DSpaceObject[]>;
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: RemoteData<DSpaceObject>;
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: RemoteData<DSpaceObject>;
/**
* Find a metadata field by key and language
*
* This method returns the value of the first element
* in the metadata array that matches the provided
* key and language
*
* @param key
* @param language
* @return string
*/
findMetadata(key: string, language?: string): string {
const metadatum = this.metadata
.find((metadatum: Metadatum) => {
return metadatum.key === key &&
(isEmpty(language) || metadatum.language === language)
});
if (isNotEmpty(metadatum)) {
return metadatum.value;
}
else {
return undefined;
}
/**
* 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((m: Metadatum) => {
return m.key === key && (isEmpty(language) || m.language === language)
});
if (isNotEmpty(metadatum)) {
return metadatum.value;
} else {
return undefined;
}
}
/**
* Find metadata by an array of keys
*
* This method returns the values of the element
* in the metadata array that match the provided
* key(s)
*
* @param key(s)
* @return Array<Metadatum>
*/
filterMetadata(keys: string[]): Metadatum[] {
return this.metadata.filter((metadatum: Metadatum) => {
return keys.some((key) => key === metadatum.key);
});
}
/**
* Find metadata by an array of keys
*
* This method returns the values of the element
* in the metadata array that match the provided
* key(s)
*
* @param key(s)
* @return Array<Metadatum>
*/
filterMetadata(keys: string[]): Array<Metadatum> {
return this.metadata
.filter((metadatum: Metadatum) => {
return keys.some(key => key === metadatum.key);
});
}
}

Some files were not shown because too many files have changed in this diff Show More