mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'upstream/master' into w2p-70504_New-user-registration
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
"path": "./webpack/webpack.common.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
},
|
||||
},
|
||||
"outputPath": "dist/browser",
|
||||
"index": "src/index.html",
|
||||
|
@@ -11,7 +11,7 @@
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||
- environment.dev.js
|
||||
- environment.dev.ts
|
||||
- Environment file for running DSpace Angular in Docker
|
||||
- local.cfg
|
||||
- Environment file for running the DSpace 7 REST API in Docker.
|
||||
|
@@ -23,4 +23,4 @@ services:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- ./environment.dev.js:/app/src/environments/environment.dev.ts
|
||||
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
||||
|
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
import { GlobalConfig } from '../src/config/global-config.interface';
|
||||
|
||||
export const environment: Partial<GlobalConfig> = {
|
||||
// This file is based on environment.template.ts provided by Angular UI
|
||||
export const environment = {
|
||||
// Default to using the local REST API (running in Docker)
|
||||
rest: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
|
@@ -1,9 +1,9 @@
|
||||
# Configuration
|
||||
|
||||
Default configuration file is located in `config/` folder. All configuration options should be listed in the default configuration file `config/environment.default.js`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change:
|
||||
Default configuration file is located in `src/environments/` folder. All configuration options should be listed in the default configuration file `src/environments/environment.common.ts`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
||||
|
||||
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
||||
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
||||
- Create a new `environment.dev.ts` file in `src/environments/` for `development` environment;
|
||||
- Create a new `environment.prod.ts` file in `src/environments/` for `production` environment;
|
||||
|
||||
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
||||
|
||||
@@ -12,8 +12,8 @@ When you start dspace-angular on node, it spins up an http server on which it li
|
||||
|
||||
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||
```
|
||||
module.exports = {
|
||||
// Angular Universal server settings.
|
||||
export const environment = {
|
||||
// Angular UI settings.
|
||||
ui: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
@@ -35,14 +35,14 @@ Alternately you can set the following environment variables. If any of these are
|
||||
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
export const environment = {
|
||||
// The REST API server settings.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'dspace7.4science.it',
|
||||
host: 'dspace7.4science.cloud',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/dspace-spring-rest/api'
|
||||
nameSpace: '/server/api'
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -50,9 +50,9 @@ module.exports = {
|
||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||
```
|
||||
DSPACE_REST_SSL=true
|
||||
DSPACE_REST_HOST=localhost
|
||||
DSPACE_REST_PORT=4000
|
||||
DSPACE_REST_NAMESPACE=/
|
||||
DSPACE_REST_HOST=dspace7.4science.cloud
|
||||
DSPACE_REST_PORT=443
|
||||
DSPACE_REST_NAMESPACE=/server/api
|
||||
```
|
||||
|
||||
## Supporting analytics services other than Google Analytics
|
||||
|
12
package.json
12
package.json
@@ -17,7 +17,7 @@
|
||||
"pree2e": "yarn run config:prod",
|
||||
"pree2e:ci": "yarn run config:prod",
|
||||
"start": "yarn run start:prod",
|
||||
"serve": "ng serve",
|
||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||
"build": "ng build",
|
||||
@@ -52,6 +52,9 @@
|
||||
"https": false
|
||||
},
|
||||
"private": true,
|
||||
"resolutions": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.2.14",
|
||||
"@angular/cdk": "8.2.3",
|
||||
@@ -88,6 +91,7 @@
|
||||
"express": "4.16.2",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"file-saver": "^1.3.8",
|
||||
"filesize": "^6.1.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"https": "1.0.0",
|
||||
"js-cookie": "2.2.0",
|
||||
@@ -146,7 +150,7 @@
|
||||
"jasmine-core": "^3.3.0",
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.1.0",
|
||||
"karma": "^5.0.9",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "2.0.1",
|
||||
@@ -157,10 +161,10 @@
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"postcss-apply": "0.11.0",
|
||||
"postcss-cssnext": "3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-responsive-type": "1.0.0",
|
||||
"postcss-smart-import": "^0.7.6",
|
||||
"protractor": "~5.4.0",
|
||||
"protractor": "^7.0.0",
|
||||
"protractor-istanbul-plugin": "2.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"rimraf": "^3.0.2",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-smart-import')(),
|
||||
require('postcss-import')(),
|
||||
require('postcss-cssnext')(),
|
||||
require('postcss-apply')(),
|
||||
require('postcss-responsive-type')()
|
||||
|
11
scripts/serve.ts
Normal file
11
scripts/serve.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { environment } from '../src/environments/environment';
|
||||
|
||||
import * as child from 'child_process';
|
||||
|
||||
/**
|
||||
* Calls `ng serve` with the following arguments configured for the UI in the environment file: host, port, nameSpace, ssl
|
||||
*/
|
||||
child.spawn(
|
||||
`ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`,
|
||||
{ stdio:'inherit', shell: true }
|
||||
);
|
211
server.ts
211
server.ts
@@ -17,26 +17,67 @@
|
||||
|
||||
import 'zone.js/dist/zone-node';
|
||||
import 'reflect-metadata';
|
||||
import 'rxjs';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as pem from 'pem';
|
||||
import * as https from 'https';
|
||||
import * as morgan from 'morgan';
|
||||
import * as express from 'express';
|
||||
import { join } from 'path';
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as compression from 'compression';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { join } from 'path';
|
||||
|
||||
import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
|
||||
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
import { environment } from './src/environments/environment';
|
||||
|
||||
// Express server
|
||||
const app = express();
|
||||
|
||||
const PORT = environment.ui.port || 4000;
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
*/
|
||||
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
||||
|
||||
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
|
||||
const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');
|
||||
|
||||
/*
|
||||
* Create a new express application
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
/*
|
||||
* If production mode is enabled in the environment file:
|
||||
* - Enable Angular's production mode
|
||||
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
|
||||
*/
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
app.use(compression());
|
||||
}
|
||||
|
||||
/*
|
||||
* Enable request logging
|
||||
* See [morgan](https://github.com/expressjs/morgan)
|
||||
*/
|
||||
app.use(morgan('dev'));
|
||||
|
||||
/*
|
||||
* Add cookie parser middleware
|
||||
* See [morgan](https://github.com/expressjs/cookie-parser)
|
||||
*/
|
||||
app.use(cookieParser());
|
||||
|
||||
/*
|
||||
* Add parser for request bodies
|
||||
* See [morgan](https://github.com/expressjs/body-parser)
|
||||
*/
|
||||
app.use(bodyParser.json());
|
||||
|
||||
/*
|
||||
* Render html pages by running angular server side
|
||||
*/
|
||||
app.engine('html', (_, options, callback) =>
|
||||
ngExpressEngine({
|
||||
bootstrap: ServerAppModuleNgFactory,
|
||||
@@ -51,25 +92,155 @@ app.engine('html', (_, options, callback) =>
|
||||
},
|
||||
provideModuleMap(LAZY_MODULE_MAP)
|
||||
],
|
||||
})(_, options, callback)
|
||||
})(_, (options as any), callback)
|
||||
);
|
||||
|
||||
/*
|
||||
* Register the view engines for html and ejs
|
||||
*/
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('view engine', 'html');
|
||||
|
||||
/*
|
||||
* Set views folder path to directory where template files are stored
|
||||
*/
|
||||
app.set('views', DIST_FOLDER);
|
||||
|
||||
// Example Express Rest API endpoints
|
||||
// app.get('/api/**', (req, res) => { });
|
||||
// Serve static files from /browser
|
||||
app.get('*.*', express.static(DIST_FOLDER, {
|
||||
maxAge: '1y'
|
||||
}));
|
||||
/*
|
||||
* Adds a cache control header to the response
|
||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
||||
*/
|
||||
function cacheControl(req, res, next) {
|
||||
// instruct browser to revalidate
|
||||
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
||||
next();
|
||||
}
|
||||
|
||||
// All regular routes use the Universal engine
|
||||
app.get('*', (req, res) => {
|
||||
res.render('index', { req });
|
||||
});
|
||||
/*
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
*/
|
||||
app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
||||
|
||||
// Start up the Node server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Node Express server listening on http://localhost:${PORT}`);
|
||||
/*
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
function ngApp(req, res) {
|
||||
// Object to be set to window.dspace when CSR is used
|
||||
// this allows us to pass the info in the original request
|
||||
// to the dspace7-angular instance running in the client's browser
|
||||
const dspace = {
|
||||
originalRequest: {
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
method: req.method,
|
||||
params: req.params,
|
||||
reportProgress: req.reportProgress,
|
||||
withCredentials: req.withCredentials,
|
||||
responseType: req.responseType,
|
||||
urlWithParams: req.urlWithParams
|
||||
}
|
||||
};
|
||||
|
||||
// callback function for the case when SSR throws an error.
|
||||
function onHandleError(parentZoneDelegate, currentZone, targetZone, error) {
|
||||
if (!res._headerSent) {
|
||||
console.warn('Error in SSR, serving for direct CSR. Error details : ', error);
|
||||
res.sendFile('index.csr.ejs', {
|
||||
root: DIST_FOLDER,
|
||||
scripts: `<script>window.dspace = ${JSON.stringify(dspace)}</script>`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (environment.universal.preboot) {
|
||||
// If preboot is enabled, create a new zone for SSR, and
|
||||
// register the error handler for when it throws an error
|
||||
Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => {
|
||||
res.render(DIST_FOLDER + '/index.html', {
|
||||
req,
|
||||
res,
|
||||
preboot: environment.universal.preboot,
|
||||
async: environment.universal.async,
|
||||
time: environment.universal.time,
|
||||
baseUrl: environment.ui.nameSpace,
|
||||
originUrl: environment.ui.baseUrl,
|
||||
requestUrl: req.originalUrl
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// If preboot is disabled, just serve the client side ejs template and pass it the required
|
||||
// variables
|
||||
console.log('Universal off, serving for direct CSR');
|
||||
res.render('index-csr.ejs', {
|
||||
root: DIST_FOLDER,
|
||||
scripts: `<script>window.dspace = ${JSON.stringify(dspace)}</script>`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register the ngApp callback function to handle incoming requests
|
||||
app.get('*', ngApp);
|
||||
|
||||
/*
|
||||
* Callback function for when the server has started
|
||||
*/
|
||||
function serverStarted() {
|
||||
console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`);
|
||||
}
|
||||
|
||||
/*
|
||||
* Create an HTTPS server with the configured port and host
|
||||
* @param keys SSL credentials
|
||||
*/
|
||||
function createHttpsServer(keys) {
|
||||
https.createServer({
|
||||
key: keys.serviceKey,
|
||||
cert: keys.certificate
|
||||
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* If SSL is enabled
|
||||
* - Read credentials from configuration files
|
||||
* - Call script to start an HTTPS server with these credentials
|
||||
* When SSL is disabled
|
||||
* - Start an HTTP server on the configured port and host
|
||||
*/
|
||||
if (environment.ui.ssl) {
|
||||
let serviceKey;
|
||||
try {
|
||||
serviceKey = fs.readFileSync('./config/ssl/key.pem');
|
||||
} catch (e) {
|
||||
console.warn('Service key not found at ./config/ssl/key.pem');
|
||||
}
|
||||
|
||||
let certificate;
|
||||
try {
|
||||
certificate = fs.readFileSync('./config/ssl/cert.pem');
|
||||
} catch (e) {
|
||||
console.warn('Certificate not found at ./config/ssl/key.pem');
|
||||
}
|
||||
|
||||
if (serviceKey && certificate) {
|
||||
createHttpsServer({
|
||||
serviceKey: serviceKey,
|
||||
certificate: certificate
|
||||
});
|
||||
} else {
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
pem.createCertificate({
|
||||
days: 1,
|
||||
selfSigned: true
|
||||
}, (error, keys) => {
|
||||
createHttpsServer(keys);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
app.listen(environment.ui.port, environment.ui.host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getAccessControlModulePath } from '../admin-routing.module';
|
||||
|
||||
const GROUP_EDIT_PATH = 'groups';
|
||||
export const GROUP_EDIT_PATH = 'groups';
|
||||
|
||||
export function getGroupEditPath(id: string) {
|
||||
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
|
||||
|
@@ -4,6 +4,7 @@ import { NgModule } from '@angular/core';
|
||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||
|
||||
@@ -14,16 +15,28 @@ export function getBitstreamFormatsModulePath() {
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
||||
{
|
||||
path: 'metadata/:schemaName',
|
||||
path: 'metadata',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: {title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata'},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: MetadataRegistryComponent
|
||||
},
|
||||
{
|
||||
path: ':schemaName',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: MetadataSchemaComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
data: {title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema'}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||
data: {title: 'admin.registries.bitstream-formats.title'}
|
||||
data: {title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats'}
|
||||
},
|
||||
])
|
||||
]
|
||||
|
@@ -4,6 +4,7 @@ import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||
import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||
@@ -17,14 +18,18 @@ const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_ADD_PATH,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AddBitstreamFormatComponent,
|
||||
data: {breadcrumbKey: 'admin.registries.bitstream-formats.create'}
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||
component: EditBitstreamFormatComponent,
|
||||
resolve: {
|
||||
bitstreamFormat: BitstreamFormatsResolver
|
||||
}
|
||||
bitstreamFormat: BitstreamFormatsResolver,
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: {breadcrumbKey: 'admin.registries.bitstream-formats.edit'}
|
||||
},
|
||||
])
|
||||
],
|
||||
|
@@ -11,7 +11,6 @@
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
|
@@ -4,7 +4,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
@@ -12,6 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { Route, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-registry',
|
||||
@@ -37,6 +39,11 @@ export class MetadataRegistryComponent {
|
||||
pageSize: 25
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not the list of MetadataSchemas needs an update
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
@@ -50,14 +57,17 @@ export class MetadataRegistryComponent {
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateSchemas();
|
||||
this.forceUpdateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of schemas by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateSchemas() {
|
||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
||||
this.metadataSchemas = this.needsUpdate$.pipe(
|
||||
filter((update) => update === true),
|
||||
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,8 +75,7 @@ export class MetadataRegistryComponent {
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.updateSchemas();
|
||||
this.needsUpdate$.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +134,7 @@ export class MetadataRegistryComponent {
|
||||
* Delete all the selected metadata schemas
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
|
@@ -21,7 +21,8 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
cancelEditMetadataSchema: () => {}
|
||||
cancelEditMetadataSchema: () => {},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
|
@@ -128,6 +128,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema) => {
|
||||
const values = {
|
||||
@@ -139,7 +140,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
this.submitForm.emit(newSchema);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
id: schema.id,
|
||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||
@@ -148,6 +149,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ describe('MetadataFieldFormComponent', () => {
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataField: () => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
|
@@ -153,6 +153,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created field using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
@@ -166,7 +167,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
this.submitForm.emit(newField);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
|
||||
id: field.id,
|
||||
schema: this.metadataSchema,
|
||||
element: (values.element ? values.element : field.element),
|
||||
@@ -177,6 +178,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -1,22 +1,23 @@
|
||||
<div class="container">
|
||||
<div class="metadata-schema row">
|
||||
<div class="col-12">
|
||||
<div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{(metadataSchema | async)?.payload?.prefix}}"</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
|
||||
|
||||
<ds-metadata-field-form
|
||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
||||
[metadataSchema]="schema"
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
|
||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||
|
||||
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
|
||||
<ds-pagination
|
||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
||||
*ngIf="fields?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataFields | async)?.payload"
|
||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
||||
[pageInfoState]="fields"
|
||||
[collectionSize]="fields?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
@@ -30,7 +31,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
||||
<tr *ngFor="let field of fields?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td>
|
||||
<label>
|
||||
@@ -39,7 +40,7 @@
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -47,14 +48,15 @@
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div *ngIf="fields?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -22,6 +22,7 @@ import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -124,7 +125,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
|
||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
|
@@ -5,7 +5,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
@@ -13,6 +13,10 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
@@ -24,21 +28,15 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
* The admin can create, edit or delete metadata fields here.
|
||||
*/
|
||||
export class MetadataSchemaComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The namespace of the metadata schema
|
||||
*/
|
||||
namespace;
|
||||
|
||||
/**
|
||||
* The metadata schema
|
||||
*/
|
||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
||||
metadataSchema$: Observable<MetadataSchema>;
|
||||
|
||||
/**
|
||||
* A list of all the fields attached to this metadata schema
|
||||
*/
|
||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
metadataFields$: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata fields
|
||||
@@ -49,6 +47,11 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
pageSizeOptions: [25, 50, 100, 200]
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not the list of MetadataFields needs an update
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
@@ -68,7 +71,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
@@ -78,18 +81,20 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFields();
|
||||
this.forceUpdateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of fields by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataSchema.subscribe((schemaData) => {
|
||||
const schema = schemaData.payload;
|
||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
||||
this.namespace = {namespace: schemaData.payload.namespace};
|
||||
});
|
||||
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||
if (update) {
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +102,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.updateFields();
|
||||
this.needsUpdate$.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +161,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* Delete all the selected metadata fields
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
|
@@ -8,7 +8,7 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
|
||||
const REGISTRIES_MODULE_PATH = 'registries';
|
||||
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
|
||||
export function getRegistriesModulePath() {
|
||||
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||
|
@@ -29,6 +29,10 @@ import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edi
|
||||
import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component';
|
||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -67,9 +71,13 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
|
||||
ItemMoveComponent,
|
||||
ItemEditBitstreamDragHandleComponent,
|
||||
VirtualMetadataComponent,
|
||||
ItemAuthorizationsComponent,
|
||||
ResourcePolicyEditComponent,
|
||||
ResourcePolicyCreateComponent,
|
||||
],
|
||||
providers: [
|
||||
BundleDataService
|
||||
BundleDataService,
|
||||
ObjectValuesPipe
|
||||
]
|
||||
})
|
||||
export class EditItemPageModule {
|
||||
|
@@ -14,6 +14,12 @@ import { ItemMoveComponent } from './item-move/item-move.component';
|
||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
|
||||
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||
import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
|
||||
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||
@@ -21,6 +27,7 @@ export const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||
export const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||
export const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||
export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||
@@ -111,12 +118,43 @@ export const ITEM_EDIT_MOVE_PATH = 'move';
|
||||
path: ITEM_EDIT_MOVE_PATH,
|
||||
component: ItemMoveComponent,
|
||||
data: { title: 'item.edit.move.title' },
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_AUTHORIZATIONS_PATH,
|
||||
children: [
|
||||
{
|
||||
path: 'create',
|
||||
resolve: {
|
||||
resourcePolicyTarget: ResourcePolicyTargetResolver
|
||||
},
|
||||
component: ResourcePolicyCreateComponent,
|
||||
data: { title: 'resource-policies.create.page.title' }
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
resolve: {
|
||||
resourcePolicy: ResourcePolicyResolver
|
||||
},
|
||||
component: ResourcePolicyEditComponent,
|
||||
data: { title: 'resource-policies.edit.page.title' }
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: ItemAuthorizationsComponent,
|
||||
data: { title: 'item.edit.authorizations.title' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: []
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService,
|
||||
ResourcePolicyResolver,
|
||||
ResourcePolicyTargetResolver
|
||||
]
|
||||
})
|
||||
export class EditItemPageRoutingModule {
|
||||
|
||||
|
@@ -0,0 +1,13 @@
|
||||
<div class="container">
|
||||
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
||||
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
|
||||
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
|
||||
<ds-resource-policies [resourceType]="'bundle'"
|
||||
[resourceUUID]="bundle.id"></ds-resource-policies>
|
||||
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
|
||||
<ds-resource-policies [resourceType]="'bitstream'"
|
||||
[resourceUUID]="bitstream.id"></ds-resource-policies>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@@ -0,0 +1,183 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
import { ItemAuthorizationsComponent } from './item-authorizations.component';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { createMockRDPaginatedObs } from '../item-bitstreams/item-bitstreams.component.spec';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||
import { getMockLinkService } from '../../../shared/mocks/link-service.mock';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { createTestComponent } from '../../../shared/testing/utils.test';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
|
||||
describe('ItemAuthorizationsComponent test suite', () => {
|
||||
let comp: ItemAuthorizationsComponent;
|
||||
let compAsAny: any;
|
||||
let fixture: ComponentFixture<ItemAuthorizationsComponent>;
|
||||
let de;
|
||||
|
||||
const linkService: any = getMockLinkService();
|
||||
|
||||
const bitstream1 = Object.assign(new Bitstream(), {
|
||||
id: 'bitstream1',
|
||||
uuid: 'bitstream1'
|
||||
});
|
||||
const bitstream2 = Object.assign(new Bitstream(), {
|
||||
id: 'bitstream2',
|
||||
uuid: 'bitstream2'
|
||||
});
|
||||
const bitstream3 = Object.assign(new Bitstream(), {
|
||||
id: 'bitstream3',
|
||||
uuid: 'bitstream3'
|
||||
});
|
||||
const bitstream4 = Object.assign(new Bitstream(), {
|
||||
id: 'bitstream4',
|
||||
uuid: 'bitstream4'
|
||||
});
|
||||
const bundle1 = Object.assign(new Bundle(), {
|
||||
id: 'bundle1',
|
||||
uuid: 'bundle1',
|
||||
_links: {
|
||||
self: { href: 'bundle1-selflink' }
|
||||
},
|
||||
bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2])
|
||||
});
|
||||
const bundle2 = Object.assign(new Bundle(), {
|
||||
id: 'bundle2',
|
||||
uuid: 'bundle2',
|
||||
_links: {
|
||||
self: { href: 'bundle2-selflink' }
|
||||
},
|
||||
bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4])
|
||||
});
|
||||
const bundles = [bundle1, bundle2];
|
||||
const bitstreamList1: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream1, bitstream2]);
|
||||
const bitstreamList2: PaginatedList<Bitstream> = new PaginatedList(new PageInfo(), [bitstream3, bitstream4]);
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
uuid: 'item',
|
||||
id: 'item',
|
||||
_links: {
|
||||
self: { href: 'item-selflink' }
|
||||
},
|
||||
bundles: createMockRDPaginatedObs([bundle1, bundle2])
|
||||
});
|
||||
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
item: createSuccessfulRemoteDataObject(item)
|
||||
})
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
ItemAuthorizationsComponent,
|
||||
TestComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
ItemAuthorizationsComponent
|
||||
],
|
||||
schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
let testComp: TestComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
const html = `
|
||||
<ds-item-authorizations></ds-item-authorizations>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testFixture.destroy();
|
||||
});
|
||||
|
||||
it('should create ItemAuthorizationsComponent', inject([ItemAuthorizationsComponent], (app: ItemAuthorizationsComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ItemAuthorizationsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = fixture.componentInstance;
|
||||
linkService.resolveLink.and.callFake((object, link) => object);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
comp = null;
|
||||
compAsAny = null;
|
||||
de = null;
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should init bundles and bitstreams map properly', () => {
|
||||
expect(compAsAny.subs.length).toBe(2);
|
||||
expect(compAsAny.bundles$.value).toEqual(bundles);
|
||||
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
||||
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
||||
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
||||
a: bitstreamList1
|
||||
}));
|
||||
|
||||
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
||||
a: bitstreamList2
|
||||
}));
|
||||
});
|
||||
|
||||
it('should get the item UUID', () => {
|
||||
|
||||
expect(comp.getItemUUID()).toBeObservable(cold('(a|)', {
|
||||
a: item.id
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should get the item\'s bundle', () => {
|
||||
|
||||
expect(comp.getItemBundles()).toBeObservable(cold('a', {
|
||||
a: bundles
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators';
|
||||
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteDataWithNotEmptyPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||
import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
|
||||
/**
|
||||
* Interface for a bundle's bitstream map entry
|
||||
*/
|
||||
interface BundleBitstreamsMapEntry {
|
||||
id: string;
|
||||
bitstreams: Observable<PaginatedList<Bitstream>>
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-authorizations',
|
||||
templateUrl: './item-authorizations.component.html'
|
||||
})
|
||||
/**
|
||||
* Component that handles the item Authorizations
|
||||
*/
|
||||
export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* A map that contains all bitstream of the item's bundles
|
||||
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
||||
*/
|
||||
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
|
||||
|
||||
/**
|
||||
* The list of bundle for the item
|
||||
* @type {Observable<PaginatedList<Bundle>>}
|
||||
*/
|
||||
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||
|
||||
/**
|
||||
* The target editing item
|
||||
* @type {Observable<Item>}
|
||||
*/
|
||||
private item$: Observable<Item>;
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {LinkService} linkService
|
||||
* @param {ActivatedRoute} route
|
||||
*/
|
||||
constructor(
|
||||
private linkService: LinkService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component, setting up the bundle and bitstream within the item
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.item$ = this.route.data.pipe(
|
||||
map((data) => data.item),
|
||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||
map((item: Item) => this.linkService.resolveLink(
|
||||
item,
|
||||
followLink('bundles', new FindListOptions(), true, followLink('bitstreams'))
|
||||
))
|
||||
) as Observable<Item>;
|
||||
|
||||
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
|
||||
filter((item: Item) => isNotEmpty(item.bundles)),
|
||||
flatMap((item: Item) => item.bundles),
|
||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
return observableOf(new PaginatedList(null, []))
|
||||
})
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
bundles$.pipe(
|
||||
take(1),
|
||||
map((list: PaginatedList<Bundle>) => list.page)
|
||||
).subscribe((bundles: Bundle[]) => {
|
||||
this.bundles$.next(bundles);
|
||||
}),
|
||||
bundles$.pipe(
|
||||
take(1),
|
||||
flatMap((list: PaginatedList<Bundle>) => list.page),
|
||||
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
||||
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
||||
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the item's UUID
|
||||
*/
|
||||
getItemUUID(): Observable<string> {
|
||||
return this.item$.pipe(
|
||||
map((item: Item) => item.id),
|
||||
first((UUID: string) => isNotEmpty(UUID))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all item's bundles
|
||||
*
|
||||
* @return an observable that emits all item's bundles
|
||||
*/
|
||||
getItemBundles(): Observable<Bundle[]> {
|
||||
return this.bundles$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all bundle's bitstreams
|
||||
*
|
||||
* @return an observable that emits all item's bundles
|
||||
*/
|
||||
private getBundleBitstreams(bundle: Bundle): Observable<PaginatedList<Bitstream>> {
|
||||
return bundle.bitstreams.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
return observableOf(new PaginatedList(null, []))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe())
|
||||
}
|
||||
}
|
@@ -36,7 +36,8 @@
|
||||
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
||||
[bundle]="bundle"
|
||||
[item]="item"
|
||||
[columnSizes]="columnSizes">
|
||||
[columnSizes]="columnSizes"
|
||||
(dropObject)="dropBitstream(bundle, $event)">
|
||||
</ds-item-edit-bitstream-bundle>
|
||||
</div>
|
||||
<div *ngIf="bundles?.length === 0"
|
||||
|
@@ -188,8 +188,21 @@ describe('ItemBitstreamsComponent', () => {
|
||||
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send out a patch for the move operations', () => {
|
||||
describe('when dropBitstream is called', () => {
|
||||
const event = {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
// tslint:disable-next-line:no-empty
|
||||
finish: () => {}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
comp.dropBitstream(bundle, event);
|
||||
});
|
||||
|
||||
it('should send out a patch for the move operation', () => {
|
||||
expect(bundleService.patch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
@@ -9,8 +9,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { zip as observableZip, of as observableOf } from 'rxjs';
|
||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
@@ -22,8 +22,6 @@ import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -90,7 +88,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
public objectCache: ObjectCacheService,
|
||||
public requestService: RequestService,
|
||||
public cdRef: ChangeDetectorRef,
|
||||
public bundleService: BundleDataService
|
||||
public bundleService: BundleDataService,
|
||||
public zone: NgZone
|
||||
) {
|
||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||
}
|
||||
@@ -143,7 +142,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
|
||||
/**
|
||||
* Submit the current changes
|
||||
* Bitstreams that were dragged around send out a patch request with move operations to the rest API
|
||||
* Bitstreams marked as deleted send out a delete request to the rest API
|
||||
* Display notifications and reset the current item/updates
|
||||
*/
|
||||
@@ -151,32 +149,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
this.submitting = true;
|
||||
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
||||
|
||||
// Fetch all move operations for each bundle
|
||||
const moveOperations$ = bundlesOnce$.pipe(
|
||||
switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) =>
|
||||
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
|
||||
take(1),
|
||||
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
|
||||
from: `/_links/bitstreams${operation.from}/href`,
|
||||
path: `/_links/bitstreams${operation.path}/href`
|
||||
}))])
|
||||
)
|
||||
)))
|
||||
);
|
||||
|
||||
// Send out an immediate patch request for each bundle
|
||||
const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe(
|
||||
switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) =>
|
||||
observableZip(...bundles.map((bundle: Bundle, index: number) => {
|
||||
if (isNotEmpty(moveOperationList[index])) {
|
||||
return this.bundleService.patch(bundle, moveOperationList[index]);
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
// Fetch all removed bitstreams from the object update service
|
||||
const removedBitstreams$ = bundlesOnce$.pipe(
|
||||
switchMap((bundles: Bundle[]) => observableZip(
|
||||
@@ -201,19 +173,42 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
);
|
||||
|
||||
// Perform the setup actions from above in order and display notifications
|
||||
patchResponses$.pipe(
|
||||
switchMap((responses: RestResponse[]) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
|
||||
return removedResponses$
|
||||
}),
|
||||
take(1)
|
||||
).subscribe((responses: RestResponse[]) => {
|
||||
removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
||||
this.reset();
|
||||
this.submitting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||
* navigate the user to the correct page)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param event The event containing the index the bitstream came from and was dropped to
|
||||
*/
|
||||
dropBitstream(bundle: Bundle, event: any) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||
const moveOperation = Object.assign({
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${event.toIndex}/href`
|
||||
});
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => {
|
||||
this.zone.run(() => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||
this.requestService.removeByHrefSubstring(bundle.self).pipe(
|
||||
filter((isCached) => isCached),
|
||||
take(1)
|
||||
).subscribe(() => event.finish());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notifications
|
||||
* - Error notification for each failed response with their message
|
||||
|
@@ -17,5 +17,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes"></ds-paginated-drag-and-drop-bitstream-list>
|
||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes" (dropObject)="dropObject.emit($event)"></ds-paginated-drag-and-drop-bitstream-list>
|
||||
</ng-template>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -36,6 +36,13 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
@Input() columnSizes: ResponsiveTableSizes;
|
||||
|
||||
/**
|
||||
* Send an event when the user drops an object on the pagination
|
||||
* The event contains details about the index the object came from and is dropped to (across the entirety of the list,
|
||||
* not just within a single page)
|
||||
*/
|
||||
@Output() dropObject: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The bootstrap sizes used for the Bundle Name column
|
||||
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
||||
|
@@ -7,18 +7,20 @@
|
||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchPage($event)">
|
||||
<ng-container *ngIf="!(loading$ | async)">
|
||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
||||
*ngVar="((updates$ | async) | dsObjectValues) as updateValues" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<div class="row bitstream-row" *ngFor="let updateValue of updateValues" cdkDrag
|
||||
[id]="updateValue.field.uuid"
|
||||
*ngVar="(updates$ | async) as updates" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<ng-container *ngIf="updates">
|
||||
<div class="row bitstream-row" *ngFor="let uuid of customOrder" cdkDrag
|
||||
[id]="uuid"
|
||||
[ngClass]="{
|
||||
'table-warning': updateValue.changeType === 0,
|
||||
'table-danger': updateValue.changeType === 2,
|
||||
'table-success': updateValue.changeType === 1,
|
||||
'bg-white': updateValue.changeType === undefined
|
||||
'table-warning': updates[uuid].changeType === 0,
|
||||
'table-danger': updates[uuid].changeType === 2,
|
||||
'table-success': updates[uuid].changeType === 1,
|
||||
'bg-white': updates[uuid].changeType === undefined
|
||||
}">
|
||||
<ds-item-edit-bitstream [fieldUpdate]="updateValue"
|
||||
<ds-item-edit-bitstream [fieldUpdate]="updates[uuid]"
|
||||
[bundleUrl]="bundle.self"
|
||||
[columnSizes]="columnSizes">
|
||||
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
|
||||
@@ -26,5 +28,8 @@
|
||||
</div>
|
||||
</ds-item-edit-bitstream>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-loading>
|
||||
</ds-pagination>
|
||||
|
@@ -16,12 +16,15 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz
|
||||
import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
|
||||
describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
let comp: PaginatedDragAndDropBitstreamListComponent;
|
||||
let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>;
|
||||
let objectUpdatesService: ObjectUpdatesService;
|
||||
let bundleService: BundleDataService;
|
||||
let objectValuesPipe: ObjectValuesPipe;
|
||||
let requestService: RequestService;
|
||||
|
||||
const columnSizes = new ResponsiveTableSizes([
|
||||
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
||||
@@ -97,15 +100,24 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
);
|
||||
|
||||
bundleService = jasmine.createSpyObj('bundleService', {
|
||||
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2]))
|
||||
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])),
|
||||
getBitstreamsEndpoint: observableOf('')
|
||||
});
|
||||
|
||||
objectValuesPipe = new ObjectValuesPipe();
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
hasByHrefObservable: observableOf(true)
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe],
|
||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: BundleDataService, useValue: bundleService }
|
||||
{ provide: BundleDataService, useValue: bundleService },
|
||||
{ provide: ObjectValuesPipe, useValue: objectValuesPipe },
|
||||
{ provide: RequestService, useValue: requestService }
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
|
@@ -8,6 +8,8 @@ import { switchMap } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||
import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-paginated-drag-and-drop-bitstream-list',
|
||||
@@ -33,8 +35,10 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
|
||||
constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected elRef: ElementRef,
|
||||
protected bundleService: BundleDataService) {
|
||||
super(objectUpdatesService, elRef);
|
||||
protected objectValuesPipe: ObjectValuesPipe,
|
||||
protected bundleService: BundleDataService,
|
||||
protected requestService: RequestService) {
|
||||
super(objectUpdatesService, elRef, objectValuesPipe);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -46,12 +50,18 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
*/
|
||||
initializeObjectsRD(): void {
|
||||
this.objectsRD$ = this.currentPage$.pipe(
|
||||
switchMap((page: number) => this.bundleService.getBitstreams(
|
||||
switchMap((page: number) => {
|
||||
const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })});
|
||||
return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe(
|
||||
switchMap((href) => this.requestService.hasByHrefObservable(href)),
|
||||
switchMap(() => this.bundleService.getBitstreams(
|
||||
this.bundle.id,
|
||||
new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}),
|
||||
paginatedOptions,
|
||||
followLink('format')
|
||||
))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -68,6 +68,7 @@ export class ItemStatusComponent implements OnInit {
|
||||
The value is supposed to be a href for the button
|
||||
*/
|
||||
this.operations = [];
|
||||
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||
if (item.isWithdrawn) {
|
||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
||||
|
@@ -7,6 +7,7 @@ import { Item } from '../core/shared/item.model';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { find } from 'rxjs/operators';
|
||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific item before the route is activated
|
||||
@@ -26,7 +27,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||
return this.itemService.findById(route.params.id,
|
||||
followLink('owningCollection'),
|
||||
followLink('bundles'),
|
||||
followLink('bundles', new FindListOptions(), true, followLink('bitstreams')),
|
||||
followLink('relationships'),
|
||||
followLink('version', undefined, true, followLink('versionhistory')),
|
||||
).pipe(
|
||||
|
@@ -7,9 +7,9 @@
|
||||
|
||||
</div>
|
||||
<div class="add">
|
||||
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/submit']" role="button">
|
||||
<button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" role="button">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -21,6 +21,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
||||
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
|
||||
@@ -54,6 +56,11 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: TranslateService, useValue: translateService },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/}
|
||||
}
|
||||
},
|
||||
ChangeDetectorRef,
|
||||
MyDSpaceNewSubmissionComponent,
|
||||
UploaderService
|
||||
@@ -86,6 +93,25 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
let fixture: ComponentFixture<MyDSpaceNewSubmissionComponent>;
|
||||
let comp: MyDSpaceNewSubmissionComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should call app.openDialog', () => {
|
||||
spyOn(comp, 'openDialog');
|
||||
const submissionButton = fixture.debugElement.query(By.css('button.btn-primary'));
|
||||
submissionButton.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
expect(comp.openDialog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
|
@@ -15,6 +15,9 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { SearchResult } from '../../shared/search/search-result.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { CreateItemParentSelectorComponent } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
|
||||
/**
|
||||
* This component represents the whole mydspace page header
|
||||
@@ -55,7 +58,9 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
||||
private halService: HALEndpointService,
|
||||
private notificationsService: NotificationsService,
|
||||
private store: Store<SubmissionState>,
|
||||
private translate: TranslateService) {
|
||||
private translate: TranslateService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +110,14 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
||||
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called on clicking the button "New Submition", It opens a dialog for
|
||||
* select a collection.
|
||||
*/
|
||||
openDialog() {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
|
@@ -33,7 +33,7 @@ export function getBitstreamModulePath() {
|
||||
return `/${BITSTREAM_MODULE_PATH}`;
|
||||
}
|
||||
|
||||
const ADMIN_MODULE_PATH = 'admin';
|
||||
export const ADMIN_MODULE_PATH = 'admin';
|
||||
|
||||
export function getAdminModulePath() {
|
||||
return `/${ADMIN_MODULE_PATH}`;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||
</ng-container>
|
||||
@@ -14,4 +16,5 @@
|
||||
<ng-template #activeBreadcrumb let-text="text">
|
||||
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
|
@@ -9,6 +9,9 @@ import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
|
||||
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
|
||||
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { VarDirective } from '../shared/utils/var.directive';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
class TestBreadcrumbsService implements BreadcrumbsService<string> {
|
||||
@@ -64,17 +67,16 @@ describe('BreadcrumbsComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BreadcrumbsComponent],
|
||||
declarations: [BreadcrumbsComponent, VarDirective],
|
||||
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
}), NgbModule],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: route}
|
||||
|
||||
]
|
||||
], schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
@@ -92,14 +94,16 @@ describe('BreadcrumbsComponent', () => {
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]))
|
||||
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]));
|
||||
});
|
||||
|
||||
it('should call resolveBreadcrumb on init', () => {
|
||||
router.events = observableOf(new NavigationEnd(0, '', ''));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveBreadcrumbs', () => {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
|
||||
import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isUndefined } from '../shared/empty.util';
|
||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
|
||||
import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component representing the breadcrumbs of a page
|
||||
@@ -13,22 +13,17 @@ import { combineLatest, Observable, Subscription, of as observableOf } from 'rxj
|
||||
templateUrl: './breadcrumbs.component.html',
|
||||
styleUrls: ['./breadcrumbs.component.scss']
|
||||
})
|
||||
export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||
export class BreadcrumbsComponent implements OnInit {
|
||||
/**
|
||||
* List of breadcrumbs for this page
|
||||
* Observable of the list of breadcrumbs for this page
|
||||
*/
|
||||
breadcrumbs: Breadcrumb[];
|
||||
breadcrumbs$: Observable<Breadcrumb[]>;
|
||||
|
||||
/**
|
||||
* Whether or not to show breadcrumbs on this page
|
||||
*/
|
||||
showBreadcrumbs: boolean;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from on destroy
|
||||
*/
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
@@ -39,14 +34,11 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||
* Sets the breadcrumbs on init for this page
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.router.events.pipe(
|
||||
this.breadcrumbs$ = this.router.events.pipe(
|
||||
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
|
||||
tap(() => this.reset()),
|
||||
switchMap(() => this.resolveBreadcrumbs(this.route.root))
|
||||
).subscribe((breadcrumbs) => {
|
||||
this.breadcrumbs = breadcrumbs;
|
||||
}
|
||||
)
|
||||
switchMap(() => this.resolveBreadcrumbs(this.route.root)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,20 +73,10 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.subscription)) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the state of the breadcrumbs
|
||||
*/
|
||||
reset() {
|
||||
this.breadcrumbs = [];
|
||||
this.showBreadcrumbs = true;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { fakeAsync, flush, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
|
||||
import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs';
|
||||
|
||||
import { AuthEffects } from './auth.effects';
|
||||
@@ -29,41 +29,53 @@ import {
|
||||
} from './auth.actions';
|
||||
import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthState } from './auth.reducer';
|
||||
|
||||
import { authReducer } from './auth.reducer';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||
import { AppState, storeModuleConfig } from '../../app.reducer';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { isAuthenticated, isAuthenticatedLoaded } from './selectors';
|
||||
|
||||
describe('AuthEffects', () => {
|
||||
let authEffects: AuthEffects;
|
||||
let actions: Observable<any>;
|
||||
let authServiceStub;
|
||||
const store: Store<AuthState> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
select: observableOf(true)
|
||||
});
|
||||
let initialState;
|
||||
let token;
|
||||
let store: MockStore<AppState>;
|
||||
|
||||
function init() {
|
||||
authServiceStub = new AuthServiceStub();
|
||||
token = authServiceStub.getToken();
|
||||
initialState = {
|
||||
core: {
|
||||
auth: {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
authMethods: []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig)
|
||||
],
|
||||
providers: [
|
||||
AuthEffects,
|
||||
provideMockStore({ initialState }),
|
||||
{ provide: AuthService, useValue: authServiceStub },
|
||||
{ provide: Store, useValue: store },
|
||||
provideMockActions(() => actions),
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
authEffects = TestBed.get(AuthEffects);
|
||||
store = TestBed.get(Store);
|
||||
});
|
||||
|
||||
describe('authenticate$', () => {
|
||||
@@ -138,7 +150,8 @@ describe('AuthEffects', () => {
|
||||
|
||||
describe('authenticatedSuccess$', () => {
|
||||
|
||||
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', () => {
|
||||
it('should return a RETRIEVE_AUTHENTICATED_EPERSON action in response to a AUTHENTICATED_SUCCESS action', (done) => {
|
||||
spyOn((authEffects as any).authService, 'storeToken');
|
||||
actions = hot('--a-', {
|
||||
a: {
|
||||
type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: {
|
||||
@@ -151,8 +164,14 @@ describe('AuthEffects', () => {
|
||||
|
||||
const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) });
|
||||
|
||||
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
||||
authEffects.authenticatedSuccess$.subscribe(() => {
|
||||
expect(authServiceStub.storeToken).toHaveBeenCalledWith(token);
|
||||
});
|
||||
|
||||
expect(authEffects.authenticatedSuccess$).toBeObservable(expected);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('checkToken$', () => {
|
||||
@@ -362,4 +381,40 @@ describe('AuthEffects', () => {
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('clearInvalidTokenOnRehydrate$', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store.overrideSelector(isAuthenticated, false);
|
||||
});
|
||||
|
||||
describe('when auth loaded is false', () => {
|
||||
it('should not call removeToken method', (done) => {
|
||||
store.overrideSelector(isAuthenticatedLoaded, false);
|
||||
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } });
|
||||
spyOn(authServiceStub, 'removeToken');
|
||||
|
||||
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
|
||||
expect(authServiceStub.removeToken).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when auth loaded is true', () => {
|
||||
it('should call removeToken method', fakeAsync(() => {
|
||||
store.overrideSelector(isAuthenticatedLoaded, true);
|
||||
actions = hot('--a-|', { a: { type: StoreActionTypes.REHYDRATE } });
|
||||
spyOn(authServiceStub, 'removeToken');
|
||||
|
||||
authEffects.clearInvalidTokenOnRehydrate$.subscribe(() => {
|
||||
expect(authServiceStub.removeToken).toHaveBeenCalled();
|
||||
flush();
|
||||
});
|
||||
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,20 +1,18 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { catchError, debounceTime, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
// import @ngrx
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import { Action, select, Store } from '@ngrx/store';
|
||||
|
||||
// import services
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
import { EPerson } from '../eperson/models/eperson.model';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { isAuthenticated, isAuthenticatedLoaded } from './selectors';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
// import actions
|
||||
@@ -64,7 +62,6 @@ export class AuthEffects {
|
||||
@Effect()
|
||||
public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
|
||||
tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
|
||||
map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
|
||||
);
|
||||
|
||||
@@ -81,6 +78,7 @@ export class AuthEffects {
|
||||
@Effect()
|
||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
);
|
||||
|
||||
@@ -172,10 +170,11 @@ export class AuthEffects {
|
||||
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$.pipe(
|
||||
ofType(StoreActionTypes.REHYDRATE),
|
||||
switchMap(() => {
|
||||
return this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
const isLoaded$ = this.store.pipe(select(isAuthenticatedLoaded));
|
||||
const authenticated$ = this.store.pipe(select(isAuthenticated));
|
||||
return observableCombineLatest(isLoaded$, authenticated$).pipe(
|
||||
take(1),
|
||||
filter((authenticated) => !authenticated),
|
||||
filter(([loaded, authenticated]) => loaded && !authenticated),
|
||||
tap(() => this.authService.removeToken()),
|
||||
tap(() => this.authService.resetAuthenticationError())
|
||||
);
|
||||
|
@@ -48,7 +48,7 @@ describe(`AuthInterceptor`, () => {
|
||||
|
||||
describe('when has a valid token', () => {
|
||||
|
||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => {
|
||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
@@ -58,8 +58,19 @@ describe(`AuthInterceptor`, () => {
|
||||
const token = httpRequest.request.headers.get('authorization');
|
||||
expect(token).toBeNull();
|
||||
});
|
||||
it('should add an Authorization header when we’re sending a HTTP request to the\'authn/logout\' endpoint', () => {
|
||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/logout', 'test').subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||
const httpRequest = httpMock.expectOne(`dspace-spring-rest/api/authn/logout`);
|
||||
|
||||
expect(httpRequest.request.headers.has('authorization'));
|
||||
const token = httpRequest.request.headers.get('authorization');
|
||||
expect(token).toBe('Bearer token_test');
|
||||
});
|
||||
|
||||
it('should add an Authorization header when we’re sending a HTTP request to a non-\'authn\' endpoint', () => {
|
||||
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||
expect(response).toBeTruthy();
|
||||
});
|
||||
|
@@ -221,7 +221,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||
// Redirect to the login route
|
||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
||||
return observableOf(null);
|
||||
} else if (!this.isAuthRequest(req) && isNotEmpty(token)) {
|
||||
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
||||
// Intercept a request that is not to the authentication endpoint
|
||||
authService.isTokenExpiring().pipe(
|
||||
filter((isExpiring) => isExpiring))
|
||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for a Collection
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
|
||||
super(breadcrumbService, dataService);
|
||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for a Community
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
|
||||
super(breadcrumbService, dataService);
|
||||
|
@@ -13,7 +13,9 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for a DSpaceObject
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
|
||||
}
|
||||
|
@@ -15,7 +15,9 @@ import { Injectable } from '@angular/core';
|
||||
/**
|
||||
* Service to calculate DSpaceObject breadcrumbs for a single part of the route
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
|
||||
constructor(
|
||||
private linkService: LinkService,
|
||||
|
@@ -28,7 +28,8 @@ export class DSONameService {
|
||||
return dso.firstMetadataValue('organization.legalName');
|
||||
},
|
||||
Default: (dso: DSpaceObject): string => {
|
||||
return dso.firstMetadataValue('dc.title');
|
||||
// If object doesn't have dc.title metadata use name property
|
||||
return dso.firstMetadataValue('dc.title') || dso.name;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -14,7 +14,7 @@ describe('I18nBreadcrumbResolver', () => {
|
||||
});
|
||||
|
||||
it('should resolve the breadcrumb config', () => {
|
||||
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any);
|
||||
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
|
||||
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
||||
expect(resolvedConfig).toEqual(expectedConfig);
|
||||
});
|
||||
|
@@ -7,7 +7,9 @@ import { hasNoValue } from '../../shared/empty.util';
|
||||
/**
|
||||
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
|
||||
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
|
||||
}
|
||||
@@ -23,7 +25,17 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
|
||||
if (hasNoValue(key)) {
|
||||
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
||||
}
|
||||
const fullPath = route.url.join('');
|
||||
const fullPath = this.getResolvedUrl(route);
|
||||
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full URL of an ActivatedRouteSnapshot
|
||||
* @param route
|
||||
*/
|
||||
getResolvedUrl(route: ActivatedRouteSnapshot): string {
|
||||
return route.pathFromRoot
|
||||
.map((v) => v.url.map((segment) => segment.toString()).join('/'))
|
||||
.join('/');
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,9 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs';
|
||||
/**
|
||||
* Service to calculate i18n breadcrumbs for a single part of the route
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class I18nBreadcrumbsService implements BreadcrumbsService<string> {
|
||||
|
||||
/**
|
||||
|
@@ -8,7 +8,9 @@ import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-con
|
||||
/**
|
||||
* The class that resolves the BreadcrumbConfig object for an Item
|
||||
*/
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
||||
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
|
||||
super(breadcrumbService, dataService);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object
|
||||
*/
|
||||
export class SearchParam {
|
||||
export class RequestParam {
|
||||
constructor(public fieldName: string, public fieldValue: any) {
|
||||
|
||||
}
|
45
src/app/core/cache/response.models.ts
vendored
45
src/app/core/cache/response.models.ts
vendored
@@ -6,9 +6,6 @@ import { ConfigObject } from '../config/models/config.model';
|
||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
||||
import { IntegrationModel } from '../integration/models/integration.model';
|
||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { SubmissionObject } from '../submission/models/submission-object.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
@@ -41,48 +38,6 @@ export class DSOSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse
|
||||
*/
|
||||
export class RegistryMetadataschemasSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadataschemasResponse: RegistryMetadataschemasResponse,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse
|
||||
*/
|
||||
export class RegistryMetadatafieldsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public metadatafieldsResponse: RegistryMetadatafieldsResponse,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse
|
||||
*/
|
||||
export class RegistryBitstreamformatsSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public bitstreamformatsResponse: RegistryBitstreamformatsResponse,
|
||||
public statusCode: number,
|
||||
public statusText: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode, statusText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A successful response containing exactly one MetadataSchema
|
||||
*/
|
||||
|
@@ -69,16 +69,11 @@ import { ItemDataService } from './data/item-data.service';
|
||||
import { LicenseDataService } from './data/license-data.service';
|
||||
import { LookupRelationService } from './data/lookup-relation.service';
|
||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||
import { MetadatafieldParsingService } from './data/metadatafield-parsing.service';
|
||||
import { MetadataschemaParsingService } from './data/metadataschema-parsing.service';
|
||||
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
||||
import { ObjectUpdatesService } from './data/object-updates/object-updates.service';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
||||
import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service';
|
||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
||||
import { RelationshipService } from './data/relationship.service';
|
||||
import { ResourcePolicyService } from './data/resource-policy.service';
|
||||
import { ResourcePolicyService } from './resource-policy/resource-policy.service';
|
||||
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
||||
import { SiteDataService } from './data/site-data.service';
|
||||
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
||||
@@ -116,7 +111,7 @@ import { RelationshipType } from './shared/item-relationships/relationship-type.
|
||||
import { Relationship } from './shared/item-relationships/relationship.model';
|
||||
import { Item } from './shared/item.model';
|
||||
import { License } from './shared/license.model';
|
||||
import { ResourcePolicy } from './shared/resource-policy.model';
|
||||
import { ResourcePolicy } from './resource-policy/models/resource-policy.model';
|
||||
import { SearchConfigurationService } from './shared/search/search-configuration.service';
|
||||
import { SearchFilterService } from './shared/search/search-filter.service';
|
||||
import { SearchService } from './shared/search/search.service';
|
||||
@@ -145,8 +140,9 @@ import { Version } from './shared/version.model';
|
||||
import { VersionHistory } from './shared/version-history.model';
|
||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||
import { EpersonRegistrationService } from './data/eperson-registration.service';
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -203,9 +199,6 @@ const PROVIDERS = [
|
||||
FacetValueResponseParsingService,
|
||||
FacetValueMapResponseParsingService,
|
||||
FacetConfigResponseParsingService,
|
||||
RegistryMetadataschemasResponseParsingService,
|
||||
RegistryMetadatafieldsResponseParsingService,
|
||||
RegistryBitstreamformatsResponseParsingService,
|
||||
MappedCollectionsReponseParsingService,
|
||||
DebugResponseParsingService,
|
||||
SearchResponseParsingService,
|
||||
@@ -225,8 +218,6 @@ const PROVIDERS = [
|
||||
JsonPatchOperationsBuilder,
|
||||
AuthorityService,
|
||||
IntegrationResponseParsingService,
|
||||
MetadataschemaParsingService,
|
||||
MetadatafieldParsingService,
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
NotificationsService,
|
||||
@@ -266,6 +257,8 @@ const PROVIDERS = [
|
||||
LicenseDataService,
|
||||
ItemTypeDataService,
|
||||
WorkflowActionDataService,
|
||||
MetadataSchemaDataService,
|
||||
MetadataFieldDataService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
|
@@ -88,10 +88,12 @@ export class BundleDataService extends DataService<Bundle> {
|
||||
/**
|
||||
* Get the bitstreams endpoint for a bundle
|
||||
* @param bundleId
|
||||
* @param searchOptions
|
||||
*/
|
||||
getBitstreamsEndpoint(bundleId: string): Observable<string> {
|
||||
getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`))
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)),
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,9 +104,8 @@ export class BundleDataService extends DataService<Bundle> {
|
||||
* @param linksToFollow The {@link FollowLinkConfig}s for the request
|
||||
*/
|
||||
getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||
const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe(
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
|
||||
|
||||
hrefObs.pipe(
|
||||
take(1)
|
||||
).subscribe((href) => {
|
||||
|
@@ -13,13 +13,19 @@ import { RequestEntry } from './request.reducer';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||
import { hot, getTestScheduler, cold } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
const url = 'fake-url';
|
||||
const collectionId = 'fake-collection-id';
|
||||
|
||||
describe('CollectionDataService', () => {
|
||||
let service: CollectionDataService;
|
||||
|
||||
let scheduler: TestScheduler;
|
||||
let requestService: RequestService;
|
||||
let translate: TranslateService;
|
||||
let notificationsService: any;
|
||||
@@ -27,6 +33,44 @@ describe('CollectionDataService', () => {
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: any;
|
||||
|
||||
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-1-1',
|
||||
name: 'test-collection-1',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://rest.api/collections/test-collection-1-1'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mockCollection2: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-2-2',
|
||||
name: 'test-collection-2',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://rest.api/collections/test-collection-2-2'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mockCollection3: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-3-3',
|
||||
name: 'test-collection-3',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://rest.api/collections/test-collection-3-3'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const queryString = 'test-string';
|
||||
const communityId = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
const pageInfo = new PageInfo();
|
||||
const array = [mockCollection1, mockCollection2, mockCollection3];
|
||||
const paginatedList = new PaginatedList(pageInfo, array);
|
||||
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||
|
||||
describe('when the requests are successful', () => {
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
@@ -74,6 +118,43 @@ describe('CollectionDataService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling getAuthorizedCollection', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
spyOn(service, 'getAuthorizedCollection').and.callThrough();
|
||||
spyOn(service, 'getAuthorizedCollectionByCommunity').and.callThrough();
|
||||
});
|
||||
|
||||
it('should proxy the call to getAuthorizedCollection', () => {
|
||||
scheduler.schedule(() => service.getAuthorizedCollection(queryString));
|
||||
scheduler.flush();
|
||||
|
||||
expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
||||
const result = service.getAuthorizedCollection(queryString)
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should proxy the call to getAuthorizedCollectionByCommunity', () => {
|
||||
scheduler.schedule(() => service.getAuthorizedCollectionByCommunity(communityId, queryString));
|
||||
scheduler.flush();
|
||||
|
||||
expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
||||
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString)
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the requests are unsuccessful', () => {
|
||||
@@ -117,7 +198,9 @@ describe('CollectionDataService', () => {
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList')
|
||||
buildList: hot('a|', {
|
||||
a: paginatedListRD
|
||||
})
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
|
@@ -12,7 +12,7 @@ import { PaginatedSearchOptions } from '../../shared/search/paginated-search-opt
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ContentSourceSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { CoreState } from '../core.reducers';
|
||||
@@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
/**
|
||||
* Get all collections the user is authorized to submit to
|
||||
*
|
||||
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollection(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findAuthorized';
|
||||
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorized';
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: [new RequestParam('query', query)]
|
||||
});
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
return this.searchBy(searchHref, options, ...linksToFollow).pipe(
|
||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||
}
|
||||
|
||||
@@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
* Get all collections the user is authorized to submit to, by community
|
||||
*
|
||||
* @param communityId The community id
|
||||
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||
* @param options The [[FindListOptions]] object
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findAuthorizedByCommunity';
|
||||
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorizedByCommunity';
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: [new SearchParam('uuid', communityId)]
|
||||
searchParams: [
|
||||
new RequestParam('uuid', communityId),
|
||||
new RequestParam('query', query)
|
||||
]
|
||||
});
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
@@ -108,7 +116,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
* true if the user has at least one collection to submit to
|
||||
*/
|
||||
hasAuthorizedCollection(): Observable<boolean> {
|
||||
const searchHref = 'findAuthorized';
|
||||
const searchHref = 'findSubmitAuthorized';
|
||||
const options = new FindListOptions();
|
||||
options.elementsPerPage = 1;
|
||||
|
||||
|
@@ -20,7 +20,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { getClassForType } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
@@ -45,11 +45,12 @@ import {
|
||||
FindListOptions,
|
||||
FindListRequest,
|
||||
GetRequest,
|
||||
PatchRequest
|
||||
PatchRequest, PutRequest
|
||||
} from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RequestService } from './request.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
|
||||
export abstract class DataService<T extends CacheableObject> {
|
||||
protected abstract requestService: RequestService;
|
||||
@@ -111,7 +112,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
result$ = this.getSearchEndpoint(searchMethod);
|
||||
|
||||
if (hasValue(options.searchParams)) {
|
||||
options.searchParams.forEach((param: SearchParam) => {
|
||||
options.searchParams.forEach((param: RequestParam) => {
|
||||
args.push(`${param.fieldName}=${param.fieldValue}`);
|
||||
})
|
||||
}
|
||||
@@ -153,6 +154,33 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an array of RequestParam into a query string and combine it with the given HREF
|
||||
*
|
||||
* @param href The HREF to which the query string should be appended
|
||||
* @param params Array with additional params to combine with query string
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*
|
||||
* @return {Observable<string>}
|
||||
* Return an observable that emits created HREF
|
||||
*/
|
||||
protected buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||
|
||||
let args = [];
|
||||
if (hasValue(params)) {
|
||||
params.forEach((param: RequestParam) => {
|
||||
args.push(`${param.fieldName}=${param.fieldValue}`);
|
||||
})
|
||||
}
|
||||
|
||||
args = this.addEmbedParams(args, ...linksToFollow);
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Adds the embed options to the link for the request
|
||||
* @param args params for the query string
|
||||
@@ -293,9 +321,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @param searchMethod The search method for the object
|
||||
*/
|
||||
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||
return this.halService.getEndpoint(`${this.linkPath}/search`).pipe(
|
||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||
filter((href: string) => isNotEmpty(href)),
|
||||
map((href: string) => `${href}/${searchMethod}`));
|
||||
map((href: string) => `${href}/search/${searchMethod}`));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,7 +344,9 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
tap((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||
request.responseMsToLive = 10 * 1000;
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
|
||||
this.requestService.configure(request);
|
||||
}
|
||||
@@ -354,6 +384,28 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PUT request for the specified object
|
||||
*
|
||||
* @param object The object to send a put request for.
|
||||
*/
|
||||
put(object: T): Observable<RemoteData<T>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||
const request = new PutRequest(requestId, object._links.self.href, serializedObject);
|
||||
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
|
||||
this.requestService.configure(request);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((re: RequestEntry) => hasValue(re) && re.completed),
|
||||
switchMap(() => this.findByHref(object._links.self.href))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new patch to the object cache
|
||||
* The patch is derived from the differences between the given object and its version in the object cache
|
||||
@@ -380,15 +432,15 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
*
|
||||
* @param {DSpaceObject} dso
|
||||
* The object to create
|
||||
* @param {string} parentUUID
|
||||
* The UUID of the parent to create the new object under
|
||||
* @param {RequestParam[]} params
|
||||
* Array with additional params to combine with query string
|
||||
*/
|
||||
create(dso: T, parentUUID: string): Observable<RemoteData<T>> {
|
||||
create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
isNotEmptyOperator(),
|
||||
distinctUntilChanged(),
|
||||
map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint)
|
||||
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
||||
);
|
||||
|
||||
const serializedDso = new DSpaceSerializer(getClassForType((dso as any).type)).serialize(dso);
|
||||
@@ -479,7 +531,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => request.completed),
|
||||
find((request: RequestEntry) => isNotEmpty(request) && request.completed),
|
||||
map((request: RequestEntry) => request.response.isSuccessful)
|
||||
);
|
||||
}
|
||||
|
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
114
src/app/core/data/metadata-field-data.service.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { CreateRequest, FindListOptions, PutRequest } from './request.models';
|
||||
import { MetadataFieldDataService } from './metadata-field-data.service';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
|
||||
describe('MetadataFieldDataService', () => {
|
||||
let metadataFieldService: MetadataFieldDataService;
|
||||
let requestService: RequestService;
|
||||
let halService: HALEndpointService;
|
||||
let notificationsService: NotificationsService;
|
||||
let schema: MetadataSchema;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
const endpoint = 'api/metadatafield/endpoint';
|
||||
|
||||
function init() {
|
||||
schema = Object.assign(new MetadataSchema(), {
|
||||
prefix: 'dc',
|
||||
namespace: 'namespace',
|
||||
_links: {
|
||||
self: { href: 'selflink' }
|
||||
}
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||
configure: {},
|
||||
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||
removeByHrefSubstring: {}
|
||||
});
|
||||
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
error: {}
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||
});
|
||||
metadataFieldService = new MetadataFieldDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('findBySchema', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(metadataFieldService, 'searchBy');
|
||||
});
|
||||
|
||||
it('should call searchBy with the correct arguments', () => {
|
||||
metadataFieldService.findBySchema(schema);
|
||||
const expectedOptions = Object.assign(new FindListOptions(), {
|
||||
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||
});
|
||||
expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOrUpdateMetadataField', () => {
|
||||
let field: MetadataField;
|
||||
|
||||
beforeEach(() => {
|
||||
field = Object.assign(new MetadataField(), {
|
||||
element: 'identifier',
|
||||
qualifier: undefined,
|
||||
schema: schema,
|
||||
_links: {
|
||||
self: { href: 'selflink' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with a new metadata field', () => {
|
||||
it('should send a CreateRequest', (done) => {
|
||||
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with an existing metadata field', () => {
|
||||
beforeEach(() => {
|
||||
field = Object.assign(field, {
|
||||
id: 'id-of-existing-field'
|
||||
});
|
||||
});
|
||||
|
||||
it('should send a PutRequest', (done) => {
|
||||
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearRequests', () => {
|
||||
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||
metadataFieldService.clearRequests().subscribe(() => {
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataFieldService as any).linkPath}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
89
src/app/core/data/metadata-field-data.service.ts
Normal file
89
src/app/core/data/metadata-field-data.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { DataService } from './data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { FindListOptions, FindListRequest } from './request.models';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(METADATA_FIELD)
|
||||
export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||
protected linkPath = 'metadatafields';
|
||||
protected searchBySchemaLinkPath = 'bySchema';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected halService: HALEndpointService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected comparator: DefaultChangeAnalyzer<MetadataField>,
|
||||
protected http: HttpClient,
|
||||
protected notificationsService: NotificationsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find metadata fields belonging to a metadata schema
|
||||
* @param schema The metadata schema to list fields for
|
||||
* @param options The options info used to retrieve the fields
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>) {
|
||||
const optionsWithSchema = Object.assign(new FindListOptions(), options, {
|
||||
searchParams: [new RequestParam('schema', schema.prefix)]
|
||||
});
|
||||
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a MetadataField
|
||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateRequest is used
|
||||
* - On update, a PutRequest is used
|
||||
* @param field The MetadataField to create or update
|
||||
*/
|
||||
createOrUpdateMetadataField(field: MetadataField): Observable<RemoteData<MetadataField>> {
|
||||
const isUpdate = hasValue(field.id);
|
||||
|
||||
if (isUpdate) {
|
||||
return this.put(field);
|
||||
} else {
|
||||
return this.create(field, new RequestParam('schemaId', field.schema.id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all metadata field requests
|
||||
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||
*/
|
||||
clearRequests(): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
tap((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
89
src/app/core/data/metadata-schema-data.service.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { MetadataSchemaDataService } from './metadata-schema-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { CreateRequest, PutRequest } from './request.models';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
|
||||
describe('MetadataSchemaDataService', () => {
|
||||
let metadataSchemaService: MetadataSchemaDataService;
|
||||
let requestService: RequestService;
|
||||
let halService: HALEndpointService;
|
||||
let notificationsService: NotificationsService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
const endpoint = 'api/metadataschema/endpoint';
|
||||
|
||||
function init() {
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2',
|
||||
configure: {},
|
||||
getByUUID: observableOf({ response: new RestResponse(true, 200, 'OK') }),
|
||||
removeByHrefSubstring: {}
|
||||
});
|
||||
halService = Object.assign(new HALEndpointServiceStub(endpoint));
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
error: {}
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: createSuccessfulRemoteDataObject$(undefined)
|
||||
});
|
||||
metadataSchemaService = new MetadataSchemaDataService(requestService, rdbService, undefined, halService, undefined, undefined, undefined, notificationsService);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
describe('createOrUpdateMetadataSchema', () => {
|
||||
let schema: MetadataSchema;
|
||||
|
||||
beforeEach(() => {
|
||||
schema = Object.assign(new MetadataSchema(), {
|
||||
prefix: 'dc',
|
||||
namespace: 'namespace',
|
||||
_links: {
|
||||
self: { href: 'selflink' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with a new metadata schema', () => {
|
||||
it('should send a CreateRequest', (done) => {
|
||||
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('called with an existing metadata schema', () => {
|
||||
beforeEach(() => {
|
||||
schema = Object.assign(schema, {
|
||||
id: 'id-of-existing-schema'
|
||||
});
|
||||
});
|
||||
|
||||
it('should send a PutRequest', (done) => {
|
||||
metadataSchemaService.createOrUpdateMetadataSchema(schema).subscribe(() => {
|
||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearRequests', () => {
|
||||
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||
metadataSchemaService.clearRequests().subscribe(() => {
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`${endpoint}/${(metadataSchemaService as any).linkPath}`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -9,37 +9,21 @@ import { CoreState } from '../core.reducers';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
|
||||
import { DataService } from './data.service';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { RequestService } from './request.service';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
class DataServiceImpl extends DataService<MetadataSchema> {
|
||||
protected linkPath = 'metadataschemas';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: ChangeAnalyzer<MetadataSchema>) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(METADATA_SCHEMA)
|
||||
export class MetadataSchemaDataService {
|
||||
private dataService: DataServiceImpl;
|
||||
export class MetadataSchemaDataService extends DataService<MetadataSchema> {
|
||||
protected linkPath = 'metadataschemas';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
@@ -50,6 +34,35 @@ export class MetadataSchemaDataService {
|
||||
protected comparator: DefaultChangeAnalyzer<MetadataSchema>,
|
||||
protected http: HttpClient,
|
||||
protected notificationsService: NotificationsService) {
|
||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a MetadataSchema
|
||||
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateRequest is used
|
||||
* - On update, a PutRequest is used
|
||||
* @param schema The MetadataSchema to create or update
|
||||
*/
|
||||
createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<RemoteData<MetadataSchema>> {
|
||||
const isUpdate = hasValue(schema.id);
|
||||
|
||||
if (isUpdate) {
|
||||
return this.put(schema);
|
||||
} else {
|
||||
return this.create(schema);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all metadata schema requests
|
||||
* Used for refreshing lists after adding/updating/removing a metadata schema in the registry
|
||||
*/
|
||||
clearRequests(): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
/**
|
||||
* A service responsible for parsing DSpaceRESTV2Response data related to a single MetadataField to a valid RestResponse
|
||||
*/
|
||||
@Injectable()
|
||||
export class MetadatafieldParsingService implements ResponseParsingService {
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const deserialized = new DSpaceSerializer(MetadataField).deserialize(payload);
|
||||
return new MetadatafieldSuccessResponse(deserialized, data.statusCode, data.statusText);
|
||||
}
|
||||
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataschemaParsingService implements ResponseParsingService {
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const deserialized = new DSpaceSerializer(MetadataSchema).deserialize(payload);
|
||||
return new MetadataschemaSuccessResponse(deserialized, data.statusCode, data.statusText);
|
||||
}
|
||||
|
||||
}
|
@@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m
|
||||
*/
|
||||
export const ObjectUpdatesActionTypes = {
|
||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||
ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'),
|
||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||
@@ -17,8 +16,7 @@ export const ObjectUpdatesActionTypes = {
|
||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
||||
MOVE: type('dspace/core/cache/object-updates/MOVE'),
|
||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = {
|
||||
export enum FieldChangeType {
|
||||
UPDATE = 0,
|
||||
ADD = 1,
|
||||
REMOVE = 2,
|
||||
MOVE = 3
|
||||
REMOVE = 2
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action {
|
||||
payload: {
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date,
|
||||
order: string[],
|
||||
pageSize: number,
|
||||
page: number
|
||||
lastModified: Date
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action {
|
||||
constructor(
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date,
|
||||
order: string[] = [],
|
||||
pageSize: number = 9999,
|
||||
page: number = 0
|
||||
lastModified: Date
|
||||
) {
|
||||
this.payload = { url, fields, lastModified, order, pageSize, page };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to initialize a new page's fields in the ObjectUpdates state
|
||||
*/
|
||||
export class AddPageToCustomOrderAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER;
|
||||
payload: {
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
order: string[],
|
||||
page: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddPageToCustomOrderAction
|
||||
*
|
||||
* @param url The unique url of the page for which the fields are being added
|
||||
* @param fields The identifiable fields of which the updates are kept track of
|
||||
* @param order A custom order to keep track of objects moving around
|
||||
* @param page The page to populate in the custom order
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
order: string[] = [],
|
||||
page: number = 0
|
||||
) {
|
||||
this.payload = { url, fields, order, page };
|
||||
this.payload = { url, fields, lastModified };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,43 +281,6 @@ export class RemoveFieldUpdateAction implements Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid
|
||||
*/
|
||||
export class MoveFieldUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.MOVE;
|
||||
payload: {
|
||||
url: string,
|
||||
from: number,
|
||||
to: number,
|
||||
fromPage: number,
|
||||
toPage: number,
|
||||
field?: Identifiable
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new RemoveObjectUpdatesAction
|
||||
*
|
||||
* @param url
|
||||
* the unique url of the page for which a field's change should be removed
|
||||
* @param from The index of the object to move
|
||||
* @param to The index to move the object to
|
||||
* @param fromPage The page to move the object from
|
||||
* @param toPage The page to move the object to
|
||||
* @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages)
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
from: number,
|
||||
to: number,
|
||||
fromPage: number,
|
||||
toPage: number,
|
||||
field?: Identifiable
|
||||
) {
|
||||
this.payload = { url, from, to, fromPage, toPage, field };
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
@@ -369,8 +293,6 @@ export type ObjectUpdatesAction
|
||||
| ReinstateObjectUpdatesAction
|
||||
| RemoveObjectUpdatesAction
|
||||
| RemoveFieldUpdateAction
|
||||
| MoveFieldUpdateAction
|
||||
| AddPageToCustomOrderAction
|
||||
| RemoveAllObjectUpdatesAction
|
||||
| SelectVirtualMetadataAction
|
||||
| SetEditableFieldUpdateAction
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
||||
InitializeFieldsAction,
|
||||
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
|
||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||
@@ -85,16 +85,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,16 +111,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
},
|
||||
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
||||
fieldStates: {
|
||||
@@ -165,16 +145,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,7 +213,7 @@ describe('objectUpdatesReducer', () => {
|
||||
});
|
||||
|
||||
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0);
|
||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
||||
|
||||
const expectedState = {
|
||||
[url]: {
|
||||
@@ -261,17 +231,7 @@ describe('objectUpdatesReducer', () => {
|
||||
},
|
||||
fieldUpdates: {},
|
||||
virtualMetadataSources: {},
|
||||
lastModified: modDate,
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
lastModified: modDate
|
||||
}
|
||||
};
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
@@ -337,30 +297,4 @@ describe('objectUpdatesReducer', () => {
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
expect(newState[url].fieldUpdates[uuid]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should move the custom order from the state when the MOVE action is dispatched', () => {
|
||||
const action = new MoveFieldUpdateAction(url, 0, 1, 0, 0);
|
||||
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
expect(newState[url].customOrder.newOrderPages[0].order[0]).toEqual(testState[url].customOrder.newOrderPages[0].order[1]);
|
||||
expect(newState[url].customOrder.newOrderPages[0].order[1]).toEqual(testState[url].customOrder.newOrderPages[0].order[0]);
|
||||
expect(newState[url].customOrder.changed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should add a new page to the custom order and add empty pages in between when the ADD_PAGE_TO_CUSTOM_ORDER action is dispatched', () => {
|
||||
const identifiable4 = {
|
||||
uuid: 'a23eae5a-7857-4ef9-8e52-989436ad2955',
|
||||
key: 'dc.description.abstract',
|
||||
language: null,
|
||||
value: 'Extra value'
|
||||
};
|
||||
const action = new AddPageToCustomOrderAction(url, [identifiable4], [identifiable4.uuid], 2);
|
||||
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
// Confirm the page in between the two pages (index 1) has been filled with 10 (page size) undefined values
|
||||
expect(newState[url].customOrder.newOrderPages[1].order.length).toEqual(10);
|
||||
expect(newState[url].customOrder.newOrderPages[1].order[0]).toBeUndefined();
|
||||
// Verify the new page is correct
|
||||
expect(newState[url].customOrder.newOrderPages[2].order[0]).toEqual(identifiable4.uuid);
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
||||
InitializeFieldsAction,
|
||||
ObjectUpdatesAction,
|
||||
ObjectUpdatesActionTypes,
|
||||
ReinstateObjectUpdatesAction,
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
SetValidFieldUpdateAction,
|
||||
SelectVirtualMetadataAction,
|
||||
} from './object-updates.actions';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { from } from 'rxjs/internal/observable/from';
|
||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||
|
||||
/**
|
||||
@@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship {
|
||||
keepRightVirtualMetadata: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom order given to the list of objects
|
||||
*/
|
||||
export interface CustomOrder {
|
||||
initialOrderPages: OrderPage[],
|
||||
newOrderPages: OrderPage[],
|
||||
pageSize: number;
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
export interface OrderPage {
|
||||
order: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated state of a single page
|
||||
*/
|
||||
@@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry {
|
||||
fieldUpdates: FieldUpdates;
|
||||
virtualMetadataSources: VirtualMetadataSources;
|
||||
lastModified: Date;
|
||||
customOrder: CustomOrder
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: {
|
||||
return addPageToCustomOrder(state, action as AddPageToCustomOrderAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||
}
|
||||
@@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
||||
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
||||
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.MOVE: {
|
||||
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
@@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const lastModifiedServer: Date = action.payload.lastModified;
|
||||
const order = action.payload.order;
|
||||
const pageSize = action.payload.pageSize;
|
||||
const page = action.payload.page;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const initialOrderPages = addOrderToPages([], order, pageSize, page);
|
||||
const newPageState = Object.assign(
|
||||
{},
|
||||
state[url],
|
||||
{ fieldStates: fieldStates },
|
||||
{ fieldUpdates: {} },
|
||||
{ virtualMetadataSources: {} },
|
||||
{ lastModified: lastModifiedServer },
|
||||
{ customOrder: {
|
||||
initialOrderPages: initialOrderPages,
|
||||
newOrderPages: initialOrderPages,
|
||||
pageSize: pageSize,
|
||||
changed: false }
|
||||
}
|
||||
{ lastModified: lastModifiedServer }
|
||||
);
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a page of objects to the state of a specific url and update a specific page of the custom order
|
||||
* @param state The current state
|
||||
* @param action The action to perform on the current state
|
||||
*/
|
||||
function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const order = action.payload.order;
|
||||
const page = action.payload.page;
|
||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
||||
const newPageState = Object.assign({}, pageState, {
|
||||
fieldStates: Object.assign({}, pageState.fieldStates, fieldStates),
|
||||
customOrder: Object.assign({}, pageState.customOrder, {
|
||||
newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page),
|
||||
initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page)
|
||||
})
|
||||
});
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new update for a specific field to the store
|
||||
* @param state The current state
|
||||
@@ -338,19 +283,9 @@ function discardObjectUpdatesFor(url: string, state: any) {
|
||||
}
|
||||
});
|
||||
|
||||
const newCustomOrder = Object.assign({}, pageState.customOrder);
|
||||
if (pageState.customOrder.changed) {
|
||||
const initialOrder = pageState.customOrder.initialOrderPages;
|
||||
if (isNotEmpty(initialOrder)) {
|
||||
newCustomOrder.newOrderPages = initialOrder;
|
||||
newCustomOrder.changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
const discardedPageState = Object.assign({}, pageState, {
|
||||
fieldUpdates: {},
|
||||
fieldStates: newFieldStates,
|
||||
customOrder: newCustomOrder
|
||||
fieldStates: newFieldStates
|
||||
});
|
||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||
}
|
||||
@@ -504,121 +439,3 @@ function createInitialFieldStates(fields: Identifiable[]) {
|
||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||
return fieldStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add a list of objects to an existing FieldStates object
|
||||
* @param fieldStates FieldStates to add states to
|
||||
* @param fields Identifiable objects The list of objects to add to the FieldStates
|
||||
*/
|
||||
function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) {
|
||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||
return fieldStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an object within the custom order of a page state
|
||||
* @param state The current state
|
||||
* @param action The move action to perform
|
||||
*/
|
||||
function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) {
|
||||
const url = action.payload.url;
|
||||
const fromIndex = action.payload.from;
|
||||
const toIndex = action.payload.to;
|
||||
const fromPage = action.payload.fromPage;
|
||||
const toPage = action.payload.toPage;
|
||||
const field = action.payload.field;
|
||||
|
||||
const pageState: ObjectUpdatesEntry = state[url];
|
||||
const initialOrderPages = pageState.customOrder.initialOrderPages;
|
||||
const customOrderPages = [...pageState.customOrder.newOrderPages];
|
||||
|
||||
// Create a copy of the custom orders for the from- and to-pages
|
||||
const fromPageOrder = [...customOrderPages[fromPage].order];
|
||||
const toPageOrder = [...customOrderPages[toPage].order];
|
||||
if (fromPage === toPage) {
|
||||
if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) {
|
||||
// Move an item from one index to another within the same page
|
||||
moveItemInArray(fromPageOrder, fromIndex, toIndex);
|
||||
// Update the custom order for this page
|
||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
||||
}
|
||||
} else {
|
||||
if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) {
|
||||
// Move an item from one index of one page to an index in another page
|
||||
transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex);
|
||||
// Update the custom order for both pages
|
||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
||||
customOrderPages[toPage] = { order: toPageOrder };
|
||||
}
|
||||
}
|
||||
|
||||
// Create a field update if it doesn't exist for this field yet
|
||||
let fieldUpdate = {};
|
||||
if (hasValue(field)) {
|
||||
fieldUpdate = pageState.fieldUpdates[field.uuid];
|
||||
if (hasNoValue(fieldUpdate)) {
|
||||
fieldUpdate = { field: field, changeType: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store's state with new values and return
|
||||
return Object.assign({}, state, { [url]: Object.assign({}, pageState, {
|
||||
fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}),
|
||||
customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) })
|
||||
})})
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within
|
||||
* @param initialOrderPages The initial list of OrderPages
|
||||
* @param customOrderPages The changed list of OrderPages
|
||||
*/
|
||||
function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) {
|
||||
let changed = false;
|
||||
initialOrderPages.forEach((orderPage: OrderPage, page: number) => {
|
||||
if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) {
|
||||
orderPage.order.forEach((id: string, index: number) => {
|
||||
if (id !== customOrderPages[page].order[index]) {
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate
|
||||
* @param initialPages The initial list of OrderPage objects
|
||||
* @param order The list of UUIDs to create a page for
|
||||
* @param pageSize The pageSize used to populate empty spacer pages
|
||||
* @param page The index of the page to add
|
||||
*/
|
||||
function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] {
|
||||
const result = [...initialPages];
|
||||
const orderPage: OrderPage = { order: order };
|
||||
if (page < result.length) {
|
||||
// The page we're trying to add already exists in the list. Overwrite it.
|
||||
result[page] = orderPage;
|
||||
} else if (page === result.length) {
|
||||
// The page we're trying to add is the next page in the list, add it.
|
||||
result.push(orderPage);
|
||||
} else {
|
||||
// The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page.
|
||||
const emptyOrder = [];
|
||||
for (let i = 0; i < pageSize; i++) {
|
||||
emptyOrder.push(undefined);
|
||||
}
|
||||
const emptyOrderPage: OrderPage = { order: emptyOrder };
|
||||
for (let i = result.length; i < page; i++) {
|
||||
result.push(emptyOrderPage);
|
||||
}
|
||||
result.push(orderPage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../core.reducers';
|
||||
import { ObjectUpdatesService } from './object-updates.service';
|
||||
import {
|
||||
AddPageToCustomOrderAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
|
||||
@@ -13,8 +12,6 @@ import { Notification } from '../../../shared/notifications/models/notification.
|
||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
||||
|
||||
describe('ObjectUpdatesService', () => {
|
||||
let service: ObjectUpdatesService;
|
||||
@@ -47,7 +44,7 @@ describe('ObjectUpdatesService', () => {
|
||||
};
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
spyOn(store, 'dispatch');
|
||||
service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer<string>());
|
||||
service = new ObjectUpdatesService(store);
|
||||
|
||||
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
||||
@@ -63,25 +60,6 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeWithCustomOrder', () => {
|
||||
const pageSize = 20;
|
||||
const page = 0;
|
||||
|
||||
it('should dispatch an INITIALIZE action with the correct URL, initial identifiables, last modified , custom order, page size and page', () => {
|
||||
service.initializeWithCustomOrder(url, identifiables, modDate, pageSize, page);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate, identifiables.map((identifiable) => identifiable.uuid), pageSize, page));
|
||||
});
|
||||
});
|
||||
|
||||
describe('addPageToCustomOrder', () => {
|
||||
const page = 2;
|
||||
|
||||
it('should dispatch an ADD_PAGE_TO_CUSTOM_ORDER action with the correct URL, identifiables, custom order and page number to add', () => {
|
||||
service.addPageToCustomOrder(url, identifiables, page);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddPageToCustomOrderAction(url, identifiables, identifiables.map((identifiable) => identifiable.uuid), page));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldUpdates', () => {
|
||||
it('should return the list of all fields, including their update if there is one', () => {
|
||||
const result$ = service.getFieldUpdates(url, identifiables);
|
||||
@@ -116,49 +94,6 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldUpdatesByCustomOrder', () => {
|
||||
beforeEach(() => {
|
||||
const fieldStates = {
|
||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
||||
};
|
||||
|
||||
const customOrder = {
|
||||
initialOrderPages: [{
|
||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
||||
}],
|
||||
newOrderPages: [{
|
||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
||||
}],
|
||||
pageSize: 20,
|
||||
changed: true
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
||||
};
|
||||
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
||||
});
|
||||
|
||||
it('should return the list of all fields, including their update if there is one, ordered by their custom order', (done) => {
|
||||
const result$ = service.getFieldUpdatesByCustomOrder(url, identifiables);
|
||||
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||
|
||||
const expectedResult = {
|
||||
[identifiable2.uuid]: { field: identifiable2, changeType: undefined },
|
||||
[identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD },
|
||||
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }
|
||||
};
|
||||
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(expectedResult);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEditable', () => {
|
||||
it('should return false if this identifiable is currently not editable in the store', () => {
|
||||
const result$ = service.isEditable(url, identifiable1.uuid);
|
||||
@@ -274,11 +209,7 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
describe('when updates are emtpy', () => {
|
||||
beforeEach(() => {
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf({
|
||||
customOrder: {
|
||||
changed: false
|
||||
}
|
||||
}))
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf({}))
|
||||
});
|
||||
|
||||
it('should return false when there are no updates', () => {
|
||||
@@ -346,44 +277,4 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMoveOperations', () => {
|
||||
beforeEach(() => {
|
||||
const fieldStates = {
|
||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
||||
};
|
||||
|
||||
const customOrder = {
|
||||
initialOrderPages: [{
|
||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
||||
}],
|
||||
newOrderPages: [{
|
||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
||||
}],
|
||||
pageSize: 20,
|
||||
changed: true
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
||||
};
|
||||
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
||||
});
|
||||
|
||||
it('should return the expected move operations', (done) => {
|
||||
const result$ = service.getMoveOperations(url);
|
||||
|
||||
const expectedResult = [
|
||||
{ op: 'move', from: '/0', path: '/2' }
|
||||
] as MoveOperation[];
|
||||
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(expectedResult);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -8,16 +8,15 @@ import {
|
||||
Identifiable,
|
||||
OBJECT_UPDATES_TRASH_PATH,
|
||||
ObjectUpdatesEntry,
|
||||
ObjectUpdatesState, OrderPage,
|
||||
ObjectUpdatesState,
|
||||
VirtualMetadataSource
|
||||
} from './object-updates.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction,
|
||||
MoveFieldUpdateAction,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction,
|
||||
SelectVirtualMetadataAction,
|
||||
@@ -25,11 +24,8 @@ import {
|
||||
SetValidFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { flatten } from '@angular/compiler';
|
||||
|
||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||
@@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel
|
||||
*/
|
||||
@Injectable()
|
||||
export class ObjectUpdatesService {
|
||||
constructor(private store: Store<CoreState>,
|
||||
private comparator: ArrayMoveChangeAnalyzer<string>) {
|
||||
|
||||
constructor(private store: Store<CoreState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,28 +61,6 @@ export class ObjectUpdatesService {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored
|
||||
* @param url The page's URL for which the changes are being mapped
|
||||
* @param fields The initial fields for the page's object
|
||||
* @param lastModified The date the object was last modified
|
||||
* @param pageSize The page size to use for adding pages to the custom order
|
||||
* @param page The first page to populate the custom order with
|
||||
*/
|
||||
initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking
|
||||
* @param url The URL for which the changes are being mapped
|
||||
* @param fields The fields to add a new page for
|
||||
* @param page The page number (starting from index 0)
|
||||
*/
|
||||
addPageToCustomOrder(url, fields: Identifiable[], page: number): void {
|
||||
this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an AddFieldUpdateAction to the store
|
||||
* @param url The page's URL for which the changes are saved
|
||||
@@ -153,7 +125,7 @@ export class ObjectUpdatesService {
|
||||
*/
|
||||
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
for (const object of initialFields) {
|
||||
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
|
||||
@@ -166,31 +138,6 @@ export class ObjectUpdatesService {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that combines the state's updates with the initial values (when there's no update),
|
||||
* sorted by their custom order to create a FieldUpdates object
|
||||
* @param url The URL of the page for which the FieldUpdates should be requested
|
||||
* @param initialFields The initial values of the fields
|
||||
* @param page The page to retrieve
|
||||
*/
|
||||
getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) {
|
||||
for (const uuid of objectEntry.customOrder.newOrderPages[page].order) {
|
||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
||||
if (isEmpty(fieldUpdate)) {
|
||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
||||
fieldUpdate = {field: identifiable, changeType: undefined};
|
||||
}
|
||||
fieldUpdates[uuid] = fieldUpdate;
|
||||
}
|
||||
}
|
||||
return fieldUpdates;
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check if a specific field is currently editable in the store
|
||||
* @param url The URL of the page on which the field resides
|
||||
@@ -260,19 +207,6 @@ export class ObjectUpdatesService {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a MoveFieldUpdateAction
|
||||
* @param url The page's URL for which the changes are saved
|
||||
* @param from The index of the object to move
|
||||
* @param to The index to move the object to
|
||||
* @param fromPage The page to move the object from
|
||||
* @param toPage The page to move the object to
|
||||
* @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages)
|
||||
*/
|
||||
saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) {
|
||||
this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||
* @param url The URL of the page on which the field resides
|
||||
@@ -387,7 +321,7 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's url to check for in the store
|
||||
*/
|
||||
hasUpdates(url: string): Observable<boolean> {
|
||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed)));
|
||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,19 +339,4 @@ export class ObjectUpdatesService {
|
||||
getLastModified(url: string): Observable<Date> {
|
||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get move operations based on the custom order
|
||||
* @param url The page's url
|
||||
*/
|
||||
getMoveOperations(url: string): Observable<MoveOperation[]> {
|
||||
return this.getObjectEntry(url).pipe(
|
||||
map((objectEntry) => objectEntry.customOrder),
|
||||
map((customOrder) => this.comparator.diff(
|
||||
flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)),
|
||||
flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order)))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,41 +0,0 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import {
|
||||
RegistryBitstreamformatsSuccessResponse
|
||||
} from '../cache/response.models';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service';
|
||||
|
||||
describe('RegistryBitstreamformatsResponseParsingService', () => {
|
||||
let service: RegistryBitstreamformatsResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
bitstreamformats: [
|
||||
{
|
||||
uuid: 'uuid-1',
|
||||
description: 'a description'
|
||||
},
|
||||
{
|
||||
uuid: 'uuid-2',
|
||||
description: 'another description'
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse);
|
||||
});
|
||||
});
|
@@ -1,25 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService {
|
||||
constructor(private dsoParser: DSOResponseParsingService) {
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const bitstreamformats = payload._embedded.bitstreamformats;
|
||||
payload.bitstreamformats = bitstreamformats;
|
||||
|
||||
const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload);
|
||||
return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page));
|
||||
}
|
||||
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import {
|
||||
RegistryMetadatafieldsSuccessResponse
|
||||
} from '../cache/response.models';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from './registry-metadatafields-response-parsing.service';
|
||||
|
||||
describe('RegistryMetadatafieldsResponseParsingService', () => {
|
||||
let service: RegistryMetadatafieldsResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
metadatafields: [
|
||||
{
|
||||
id: 1,
|
||||
element: 'element',
|
||||
qualifier: 'qualifier',
|
||||
scopeNote: 'a scope note',
|
||||
_embedded: {
|
||||
schema: {
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
element: 'secondelement',
|
||||
qualifier: 'secondqualifier',
|
||||
scopeNote: 'a second scope note',
|
||||
_embedded: {
|
||||
schema: {
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
const emptyData = Object.assign({
|
||||
payload: {}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryMetadatafieldsResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
||||
});
|
||||
|
||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
||||
const response = service.parse(null, emptyData);
|
||||
expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse);
|
||||
});
|
||||
});
|
@@ -1,34 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService {
|
||||
constructor(private dsoParser: DSOResponseParsingService) {
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
let metadatafields = [];
|
||||
|
||||
if (hasValue(payload._embedded)) {
|
||||
metadatafields = payload._embedded.metadatafields;
|
||||
metadatafields.forEach((field) => {
|
||||
field.schema = field._embedded.schema;
|
||||
});
|
||||
}
|
||||
|
||||
payload.metadatafields = metadatafields;
|
||||
|
||||
const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload);
|
||||
return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
|
||||
}
|
||||
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { RegistryMetadataschemasSuccessResponse } from '../cache/response.models';
|
||||
|
||||
describe('RegistryMetadataschemasResponseParsingService', () => {
|
||||
let service: RegistryMetadataschemasResponseParsingService;
|
||||
|
||||
const mockDSOParser = Object.assign({
|
||||
processPageInfo: () => new PageInfo()
|
||||
}) as DSOResponseParsingService;
|
||||
|
||||
const data = Object.assign({
|
||||
payload: {
|
||||
_embedded: {
|
||||
metadataschemas: [
|
||||
{
|
||||
id: 1,
|
||||
prefix: 'test',
|
||||
namespace: 'test namespace'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
prefix: 'second',
|
||||
namespace: 'second test namespace'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
const emptyData = Object.assign({
|
||||
payload: {}
|
||||
}) as DSpaceRESTV2Response;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RegistryMetadataschemasResponseParsingService(mockDSOParser);
|
||||
});
|
||||
|
||||
it('should parse the data correctly', () => {
|
||||
const response = service.parse(null, data);
|
||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
||||
});
|
||||
|
||||
it('should not produce an error and parse the data correctly when the data is empty', () => {
|
||||
const response = service.parse(null, emptyData);
|
||||
expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse);
|
||||
});
|
||||
});
|
@@ -1,29 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService {
|
||||
constructor(private dsoParser: DSOResponseParsingService) {
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
let metadataschemas = [];
|
||||
if (hasValue(payload._embedded)) {
|
||||
metadataschemas = payload._embedded.metadataschemas;
|
||||
}
|
||||
payload.metadataschemas = metadataschemas;
|
||||
|
||||
const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload);
|
||||
return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload));
|
||||
}
|
||||
|
||||
}
|
@@ -21,7 +21,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { CoreState } from '../core.reducers';
|
||||
@@ -257,7 +257,7 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
if (options) {
|
||||
findListOptions = Object.assign(new FindListOptions(), options);
|
||||
}
|
||||
const searchParams = [new SearchParam('label', label), new SearchParam('dso', item.id)];
|
||||
const searchParams = [new RequestParam('label', label), new RequestParam('dso', item.id)];
|
||||
if (findListOptions.searchParams) {
|
||||
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
|
||||
} else {
|
||||
|
@@ -11,11 +11,9 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
|
||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
||||
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
|
||||
import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||
@@ -146,7 +144,7 @@ export class FindListOptions {
|
||||
elementsPerPage?: number;
|
||||
currentPage?: number;
|
||||
sort?: SortOptions;
|
||||
searchParams?: SearchParam[];
|
||||
searchParams?: RequestParam[];
|
||||
startsWith?: string;
|
||||
}
|
||||
|
||||
@@ -251,58 +249,6 @@ export class IntegrationRequest extends GetRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a MetadataSchema
|
||||
*/
|
||||
export class CreateMetadataSchemaRequest extends PostRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadataschemaParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update a MetadataSchema
|
||||
*/
|
||||
export class UpdateMetadataSchemaRequest extends PutRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadataschemaParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to create a MetadataField
|
||||
*/
|
||||
export class CreateMetadataFieldRequest extends PostRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadatafieldParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update a MetadataField
|
||||
*/
|
||||
export class UpdateMetadataFieldRequest extends PutRequest {
|
||||
constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) {
|
||||
super(uuid, href, body, options);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return MetadatafieldParsingService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing a submission HTTP GET request object
|
||||
*/
|
||||
|
@@ -201,8 +201,9 @@ export class RequestService {
|
||||
* Remove all request cache providing (part of) the href
|
||||
* This also includes href-to-uuid index cache
|
||||
* @param href A substring of the request(s) href
|
||||
* @return Returns an observable emitting whether or not the cache is removed
|
||||
*/
|
||||
removeByHrefSubstring(href: string) {
|
||||
removeByHrefSubstring(href: string): Observable<boolean> {
|
||||
this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
take(1)
|
||||
@@ -213,6 +214,11 @@ export class RequestService {
|
||||
});
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
|
||||
|
||||
return this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
map((uuids) => isEmpty(uuids))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,75 +0,0 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ResourcePolicy } from '../shared/resource-policy.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { ResourcePolicyService } from './resource-policy.service';
|
||||
|
||||
describe('ResourcePolicyService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: ResourcePolicyService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
const testObject = {
|
||||
uuid: '664184ee-b254-45e8-970d-220e5ccc060b'
|
||||
} as ResourcePolicy;
|
||||
const requestURL = `https://rest.api/rest/api/resourcepolicies/${testObject.uuid}`;
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
})
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const halService = {} as HALEndpointService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
|
||||
service = new ResourcePolicyService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
|
||||
spyOn((service as any).dataService, 'findByHref').and.callThrough();
|
||||
});
|
||||
|
||||
describe('findByHref', () => {
|
||||
it('should proxy the call to dataservice.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByHref(requestURL));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {
|
||||
const result = service.findByHref(requestURL);
|
||||
const expected = cold('a', {
|
||||
a: {
|
||||
payload: testObject
|
||||
}
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,95 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
|
||||
import { DataService } from '../data/data.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { Collection } from '../shared/collection.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { ResourcePolicy } from '../shared/resource-policy.model';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RESOURCE_POLICY } from '../shared/resource-policy.resource-type';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
class DataServiceImpl extends DataService<ResourcePolicy> {
|
||||
protected linkPath = 'resourcepolicies';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: ChangeAnalyzer<ResourcePolicy>) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the resourcepolicies endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(RESOURCE_POLICY)
|
||||
export class ResourcePolicyService {
|
||||
private dataService: DataServiceImpl;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<ResourcePolicy>) {
|
||||
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of {@link RemoteData} of a {@link ResourcePolicy}, based on an href, with a list of {@link FollowLinkConfig},
|
||||
* to automatically resolve {@link HALLink}s of the {@link ResourcePolicy}
|
||||
* @param href The url of {@link ResourcePolicy} we want to retrieve
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<ResourcePolicy>> {
|
||||
return this.dataService.findByHref(href, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of observables of {@link RemoteData} of {@link ResourcePolicy}s, based on an href, with a list of {@link FollowLinkConfig},
|
||||
* to automatically resolve {@link HALLink}s of the {@link ResourcePolicy}
|
||||
* @param href The url of the {@link ResourcePolicy} we want to retrieve
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<ResourcePolicy>>): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
|
||||
return this.dataService.findAllByHref(href, findListOptions, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the defaultAccessConditions {@link ResourcePolicy} list for a given {@link Collection}
|
||||
*
|
||||
* @param collection the {@link Collection} to retrieve the defaultAccessConditions for
|
||||
* @param findListOptions the {@link FindListOptions} for the request
|
||||
*/
|
||||
getDefaultAccessConditionsFor(collection: Collection, findListOptions?: FindListOptions): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
|
||||
return this.dataService.findAllByHref(collection._links.defaultAccessConditions.href, findListOptions);
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from '../../+admin/admin-access-control/epeople-registry/epeople-registry.actions';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
@@ -105,7 +105,7 @@ describe('EPersonDataService', () => {
|
||||
it('search by default scope (byMetadata) and no query', () => {
|
||||
service.searchByScope(null, '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', ''))]
|
||||
searchParams: [Object.assign(new RequestParam('query', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
@@ -113,7 +113,7 @@ describe('EPersonDataService', () => {
|
||||
it('search metadata scope and no query', () => {
|
||||
service.searchByScope('metadata', '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', ''))]
|
||||
searchParams: [Object.assign(new RequestParam('query', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
@@ -121,7 +121,7 @@ describe('EPersonDataService', () => {
|
||||
it('search metadata scope and with query', () => {
|
||||
service.searchByScope('metadata', 'test');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', 'test'))]
|
||||
searchParams: [Object.assign(new RequestParam('query', 'test'))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe('EPersonDataService', () => {
|
||||
it('search email scope and no query', () => {
|
||||
service.searchByScope('email', '');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('email', ''))]
|
||||
searchParams: [Object.assign(new RequestParam('email', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byEmail', options);
|
||||
});
|
||||
|
@@ -15,7 +15,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { DataService } from '../data/data.service';
|
||||
@@ -97,7 +97,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
* @param linksToFollow
|
||||
*/
|
||||
private getEpeopleByEmail(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
const searchParams = [new SearchParam('email', query)];
|
||||
const searchParams = [new RequestParam('email', query)];
|
||||
return this.getEPeopleBy(searchParams, this.searchByEmailPath, options, ...linksToFollow);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
* @param linksToFollow
|
||||
*/
|
||||
private getEpeopleByMetadata(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
const searchParams = [new SearchParam('query', query)];
|
||||
const searchParams = [new RequestParam('query', query)];
|
||||
return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, ...linksToFollow);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
* @param options
|
||||
* @param linksToFollow
|
||||
*/
|
||||
private getEPeopleBy(searchParams: SearchParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
private getEPeopleBy(searchParams: RequestParam[], searchMethod: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
let findListOptions = new FindListOptions();
|
||||
if (options) {
|
||||
findListOptions = Object.assign(new FindListOptions(), options);
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
GroupRegistryEditGroupAction
|
||||
} from '../../+admin/admin-access-control/group-registry/group-registry.actions';
|
||||
import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
@@ -103,7 +103,7 @@ describe('GroupDataService', () => {
|
||||
it('search with empty query', () => {
|
||||
service.searchGroups('');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', ''))]
|
||||
searchParams: [Object.assign(new RequestParam('query', ''))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
@@ -111,7 +111,7 @@ describe('GroupDataService', () => {
|
||||
it('search with query', () => {
|
||||
service.searchGroups('test');
|
||||
const options = Object.assign(new FindListOptions(), {
|
||||
searchParams: [Object.assign(new SearchParam('query', 'test'))]
|
||||
searchParams: [Object.assign(new RequestParam('query', 'test'))]
|
||||
});
|
||||
expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options);
|
||||
});
|
||||
|
@@ -14,7 +14,7 @@ import { hasValue } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { DataService } from '../data/data.service';
|
||||
@@ -97,7 +97,7 @@ export class GroupDataService extends DataService<Group> {
|
||||
* @param linksToFollow
|
||||
*/
|
||||
public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<Group>>): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
const searchParams = [new SearchParam('query', query)];
|
||||
const searchParams = [new RequestParam('query', query)];
|
||||
let findListOptions = new FindListOptions();
|
||||
if (options) {
|
||||
findListOptions = Object.assign(new FindListOptions(), options);
|
||||
@@ -121,7 +121,7 @@ export class GroupDataService extends DataService<Group> {
|
||||
isMemberOf(groupName: string): Observable<boolean> {
|
||||
const searchHref = 'isMemberOf';
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [new SearchParam('groupName', groupName)];
|
||||
options.searchParams = [new RequestParam('groupName', groupName)];
|
||||
|
||||
return this.searchBy(searchHref, options).pipe(
|
||||
filter((groups: RemoteData<PaginatedList<Group>>) => !groups.isResponsePending),
|
||||
|
@@ -1,24 +0,0 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type';
|
||||
import { HALLink } from '../shared/hal-link.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { link } from '../cache/builders/build-decorators';
|
||||
|
||||
export class RegistryBitstreamformatsResponse {
|
||||
@autoserialize
|
||||
page: PageInfo;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this RegistryBitstreamformatsResponse
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink;
|
||||
bitstreamformats: HALLink;
|
||||
};
|
||||
|
||||
@link(BITSTREAM_FORMAT)
|
||||
bitstreamformats?: BitstreamFormat[];
|
||||
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../cache/builders/build-decorators';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||
import { HALLink } from '../shared/hal-link.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
|
||||
/**
|
||||
* Class that represents a response with a registry's metadata fields
|
||||
*/
|
||||
@typedObject
|
||||
export class RegistryMetadatafieldsResponse {
|
||||
static type = METADATA_FIELD;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* List of metadata fields in the response
|
||||
*/
|
||||
@deserialize
|
||||
metadatafields: MetadataField[];
|
||||
|
||||
/**
|
||||
* Page info of this response
|
||||
*/
|
||||
@autoserialize
|
||||
page: PageInfo;
|
||||
|
||||
/**
|
||||
* The REST link to this response
|
||||
*/
|
||||
@autoserialize
|
||||
self: string;
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink,
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
|
||||
export class RegistryMetadataschemasResponse {
|
||||
@deserialize
|
||||
metadataschemas: MetadataSchema[];
|
||||
|
||||
@autoserialize
|
||||
page: PageInfo;
|
||||
|
||||
@autoserialize
|
||||
self: string;
|
||||
}
|
@@ -3,8 +3,7 @@ import { Component } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import {
|
||||
MetadataRegistryCancelFieldAction,
|
||||
MetadataRegistryCancelSchemaAction,
|
||||
@@ -17,30 +16,20 @@ import {
|
||||
MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { StoreMock } from '../../shared/testing/store.mock';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
|
||||
import {
|
||||
RegistryMetadatafieldsSuccessResponse,
|
||||
RegistryMetadataschemasSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response.models';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||
import { RegistryService } from './registry.service';
|
||||
import { storeModuleConfig } from '../../app.reducer';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent {
|
||||
@@ -49,12 +38,20 @@ class DummyComponent {
|
||||
describe('RegistryService', () => {
|
||||
let registryService: RegistryService;
|
||||
let mockStore;
|
||||
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-service-spec-pagination',
|
||||
pageSize: 20
|
||||
let metadataSchemaService: MetadataSchemaDataService;
|
||||
let metadataFieldService: MetadataFieldDataService;
|
||||
|
||||
let options: FindListOptions;
|
||||
let mockSchemasList: MetadataSchema[];
|
||||
let mockFieldsList: MetadataField[];
|
||||
|
||||
function init() {
|
||||
options = Object.assign(new FindListOptions(), {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 20
|
||||
});
|
||||
|
||||
const mockSchemasList = [
|
||||
mockSchemasList = [
|
||||
Object.assign(new MetadataSchema(), {
|
||||
id: 1,
|
||||
_links: {
|
||||
@@ -74,7 +71,8 @@ describe('RegistryService', () => {
|
||||
type: MetadataSchema.type
|
||||
})
|
||||
];
|
||||
const mockFieldsList = [
|
||||
|
||||
mockFieldsList = [
|
||||
Object.assign(new MetadataField(),
|
||||
{
|
||||
id: 1,
|
||||
@@ -125,135 +123,84 @@ describe('RegistryService', () => {
|
||||
})
|
||||
];
|
||||
|
||||
const pageInfo = new PageInfo();
|
||||
pageInfo.elementsPerPage = 20;
|
||||
pageInfo.currentPage = 1;
|
||||
metadataSchemaService = jasmine.createSpyObj('metadataSchemaService', {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
|
||||
findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
clearRequests: observableOf('href')
|
||||
});
|
||||
|
||||
const endpoint = 'path';
|
||||
const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
||||
const fieldEndpointWithParams = `${endpoint}?schema=${mockSchemasList[0].prefix}&size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`;
|
||||
|
||||
const halServiceStub = {
|
||||
getEndpoint: (link: string) => observableOf(endpoint)
|
||||
};
|
||||
|
||||
const rdbStub = {
|
||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
||||
return observableCombineLatest(requestEntryObs,
|
||||
payloadObs).pipe(map(([req, pay]) => {
|
||||
return { req, pay };
|
||||
})
|
||||
);
|
||||
},
|
||||
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => {
|
||||
return createSuccessfulRemoteDataObject$([]);
|
||||
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
|
||||
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
clearRequests: observableOf('href')
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot()],
|
||||
declarations: [
|
||||
DummyComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: RequestService, useValue: getMockRequestService() },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbStub },
|
||||
{ provide: HALEndpointService, useValue: halServiceStub },
|
||||
{ provide: Store, useClass: StoreMock },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: MetadataSchemaDataService, useValue: metadataSchemaService },
|
||||
{ provide: MetadataFieldDataService, useValue: metadataFieldService },
|
||||
RegistryService
|
||||
]
|
||||
});
|
||||
registryService = TestBed.get(RegistryService);
|
||||
mockStore = TestBed.get(Store);
|
||||
spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint));
|
||||
});
|
||||
|
||||
describe('when requesting metadataschemas', () => {
|
||||
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), {
|
||||
metadataschemas: mockSchemasList,
|
||||
page: pageInfo
|
||||
});
|
||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||
/* tslint:disable:no-empty */
|
||||
registryService.getMetadataSchemas(pagination).subscribe((value) => {
|
||||
});
|
||||
/* tslint:enable:no-empty */
|
||||
result = registryService.getMetadataSchemas(options);
|
||||
});
|
||||
|
||||
it('should call getEndpoint on the halService', () => {
|
||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||
it('should call metadataSchemaService.findAll', (done) => {
|
||||
result.subscribe(() => {
|
||||
expect(metadataSchemaService.findAll).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should send out the request on the request service', () => {
|
||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call getByHref on the request service with the correct request url', () => {
|
||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when requesting metadataschema by name', () => {
|
||||
const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), {
|
||||
metadataschemas: mockSchemasList,
|
||||
page: pageInfo
|
||||
});
|
||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||
/* tslint:disable:no-empty */
|
||||
registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => {
|
||||
});
|
||||
/* tslint:enable:no-empty */
|
||||
result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix);
|
||||
});
|
||||
|
||||
it('should call getEndpoint on the halService', () => {
|
||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||
it('should call metadataSchemaService.findById with the correct ID', (done) => {
|
||||
result.subscribe(() => {
|
||||
expect(metadataSchemaService.findById).toHaveBeenCalledWith(`${mockSchemasList[0].id}`);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should send out the request on the request service', () => {
|
||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call getByHref on the request service with the correct request url', () => {
|
||||
expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when requesting metadatafields', () => {
|
||||
const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), {
|
||||
metadatafields: mockFieldsList,
|
||||
page: pageInfo
|
||||
});
|
||||
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||
/* tslint:disable:no-empty */
|
||||
registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => {
|
||||
});
|
||||
/* tslint:enable:no-empty */
|
||||
result = registryService.getAllMetadataFields();
|
||||
});
|
||||
|
||||
it('should call getEndpoint on the halService', () => {
|
||||
expect((registryService as any).halService.getEndpoint).toHaveBeenCalled();
|
||||
it('should call metadataFieldService.findAll', (done) => {
|
||||
result.subscribe(() => {
|
||||
expect(metadataFieldService.findAll).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should send out the request on the request service', () => {
|
||||
expect((registryService as any).requestService.configure).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call getByHref on the request service with the correct request url', () => {
|
||||
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(fieldEndpointWithParams);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -370,9 +317,10 @@ describe('RegistryService', () => {
|
||||
result = registryService.createOrUpdateMetadataSchema(mockSchemasList[0]);
|
||||
});
|
||||
|
||||
it('should return the created/updated metadata schema', () => {
|
||||
it('should return the created/updated metadata schema', (done) => {
|
||||
result.subscribe((schema: MetadataSchema) => {
|
||||
expect(schema).toEqual(mockSchemasList[0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -384,9 +332,10 @@ describe('RegistryService', () => {
|
||||
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
|
||||
});
|
||||
|
||||
it('should return the created/updated metadata field', () => {
|
||||
it('should return the created/updated metadata field', (done) => {
|
||||
result.subscribe((field: MetadataField) => {
|
||||
expect(field).toEqual(mockFieldsList[0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -425,7 +374,7 @@ describe('RegistryService', () => {
|
||||
});
|
||||
|
||||
it('should remove the requests related to metadata schemas from cache', () => {
|
||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
||||
expect(metadataSchemaService.clearRequests).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -435,7 +384,7 @@ describe('RegistryService', () => {
|
||||
});
|
||||
|
||||
it('should remove the requests related to metadata fields from cache', () => {
|
||||
expect((registryService as any).requestService.removeByHrefSubstring).toHaveBeenCalled();
|
||||
expect(metadataFieldService.clearRequests).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,37 +2,18 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import {
|
||||
CreateMetadataFieldRequest,
|
||||
CreateMetadataSchemaRequest,
|
||||
DeleteRequest,
|
||||
GetRequest,
|
||||
RestRequest,
|
||||
UpdateMetadataFieldRequest,
|
||||
UpdateMetadataSchemaRequest
|
||||
} from '../data/request.models';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||
import {
|
||||
MetadatafieldSuccessResponse,
|
||||
MetadataschemaSuccessResponse,
|
||||
RegistryMetadatafieldsSuccessResponse,
|
||||
RegistryMetadataschemasSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response.models';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
|
||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||
@@ -48,15 +29,14 @@ import {
|
||||
MetadataRegistrySelectFieldAction,
|
||||
MetadataRegistrySelectSchemaAction
|
||||
} from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions';
|
||||
import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators';
|
||||
import { flatMap, map, tap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { getClassForType } from '../cache/builders/build-decorators';
|
||||
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||
@@ -70,221 +50,64 @@ const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelec
|
||||
@Injectable()
|
||||
export class RegistryService {
|
||||
|
||||
private metadataSchemasPath = 'metadataschemas';
|
||||
private metadataFieldsPath = 'metadatafields';
|
||||
|
||||
// private bitstreamFormatsPath = 'bitstreamformats';
|
||||
|
||||
constructor(protected requestService: RequestService,
|
||||
private rdb: RemoteDataBuildService,
|
||||
private halService: HALEndpointService,
|
||||
private store: Store<AppState>,
|
||||
constructor(private store: Store<AppState>,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService) {
|
||||
private translateService: TranslateService,
|
||||
private metadataSchemaService: MetadataSchemaDataService,
|
||||
private metadataFieldService: MetadataFieldDataService) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all metadata schemas
|
||||
* @param pagination The pagination info used to retrieve the schemas
|
||||
* @param options The options used to retrieve the schemas
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
public getMetadataSchemas(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
|
||||
const requestObs = this.getMetadataSchemasRequestObs(pagination);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
|
||||
);
|
||||
|
||||
const metadataschemasObs: Observable<MetadataSchema[]> = rmrObs.pipe(
|
||||
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas)
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo)
|
||||
);
|
||||
|
||||
const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe(
|
||||
map(([metadataschemas, pageInfo]) => {
|
||||
return new PaginatedList(pageInfo, metadataschemas);
|
||||
})
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
public getMetadataSchemas(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<PaginatedList<MetadataSchema>>> {
|
||||
return this.metadataSchemaService.findAll(options, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a metadata schema by its name
|
||||
* @param schemaName The name of the schema to find
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
public getMetadataSchemaByName(schemaName: string): Observable<RemoteData<MetadataSchema>> {
|
||||
// Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
||||
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'all-metadatafields-pagination',
|
||||
pageSize: 10000
|
||||
public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<MetadataSchema>> {
|
||||
// Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
||||
const options: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 10000
|
||||
});
|
||||
const requestObs = this.getMetadataSchemasRequestObs(pagination);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
return this.getMetadataSchemas(options).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
|
||||
isNotEmptyOperator(),
|
||||
map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]),
|
||||
flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow))
|
||||
);
|
||||
|
||||
const rmrObs: Observable<RegistryMetadataschemasResponse> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse)
|
||||
);
|
||||
|
||||
const metadataschemaObs: Observable<MetadataSchema> = rmrObs.pipe(
|
||||
map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas),
|
||||
map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0])
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves all metadata fields that belong to a certain metadata schema
|
||||
* @param schema The schema to filter by
|
||||
* @param pagination The pagination info used to retrieve the fields
|
||||
* @param options The options info used to retrieve the fields
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
|
||||
);
|
||||
|
||||
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
|
||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields)
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
|
||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
|
||||
);
|
||||
|
||||
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
|
||||
map(([metadatafields, pageInfo]) => {
|
||||
return new PaginatedList(pageInfo, metadatafields);
|
||||
})
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
public getMetadataFieldsBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all existing metadata fields as a paginated list
|
||||
* @param pagination Pagination options to determine which page of metadata fields should be requested
|
||||
* When no pagination is provided, all metadata fields are requested in one large page
|
||||
* @param options Options to determine which page of metadata fields should be requested
|
||||
* When no options are provided, all metadata fields are requested in one large page
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||
* @returns an observable that emits a remote data object with a page of metadata fields
|
||||
*/
|
||||
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
if (hasNoValue(pagination)) {
|
||||
pagination = {currentPage: 1, pageSize: 10000} as any;
|
||||
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||
if (hasNoValue(options)) {
|
||||
options = {currentPage: 1, elementsPerPage: 10000} as any;
|
||||
}
|
||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const rmrObs: Observable<RegistryMetadatafieldsResponse> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse)
|
||||
);
|
||||
|
||||
const metadatafieldsObs: Observable<MetadataField[]> = rmrObs.pipe(
|
||||
map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields),
|
||||
/* Make sure to explicitly cast this into a MetadataField object, on first page loads this object comes from the object cache created by the server and its prototype is unknown */
|
||||
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => Object.assign(new MetadataField(), metadataField)))
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
|
||||
map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo)
|
||||
);
|
||||
|
||||
const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe(
|
||||
map(([metadatafields, pageInfo]) => {
|
||||
return new PaginatedList(pageInfo, metadatafields);
|
||||
})
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
}
|
||||
|
||||
public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
args.push(`size=${pagination.pageSize}`);
|
||||
args.push(`page=${pagination.currentPage - 1}`);
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return RegistryMetadataschemasResponseParsingService;
|
||||
}
|
||||
});
|
||||
}),
|
||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||
);
|
||||
}
|
||||
|
||||
private getMetadataFieldsBySchemaRequestObs(pagination: PaginationComponentOptions, schema: MetadataSchema): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath + '/search/bySchema').pipe(
|
||||
// return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
args.push(`schema=${schema.prefix}`);
|
||||
args.push(`size=${pagination.pageSize}`);
|
||||
args.push(`page=${pagination.currentPage - 1}`);
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return RegistryMetadatafieldsResponseParsingService;
|
||||
}
|
||||
});
|
||||
}),
|
||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||
);
|
||||
}
|
||||
|
||||
private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
args.push(`size=${pagination.pageSize}`);
|
||||
args.push(`page=${pagination.currentPage - 1}`);
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return RegistryMetadatafieldsResponseParsingService;
|
||||
}
|
||||
});
|
||||
}),
|
||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||
);
|
||||
return this.metadataFieldService.findAll(options, ...linksToFollow);
|
||||
}
|
||||
|
||||
public editMetadataSchema(schema: MetadataSchema) {
|
||||
@@ -386,59 +209,17 @@ export class RegistryService {
|
||||
* Create or Update a MetadataSchema
|
||||
* If the MetadataSchema contains an id, it is assumed the schema already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateMetadataSchemaRequest is used
|
||||
* - On update, a UpdateMetadataSchemaRequest is used
|
||||
* - On creation, a CreateRequest is used
|
||||
* - On update, a PutRequest is used
|
||||
* @param schema The MetadataSchema to create or update
|
||||
*/
|
||||
public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable<MetadataSchema> {
|
||||
const isUpdate = hasValue(schema.id);
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => {
|
||||
if (isUpdate) {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options);
|
||||
} else {
|
||||
return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post/put request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
// Return created/updated schema
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RestResponse) => {
|
||||
if (!response.isSuccessful) {
|
||||
if (hasValue((response as any).errorMessage)) {
|
||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
||||
}
|
||||
} else {
|
||||
return this.metadataSchemaService.createOrUpdateMetadataSchema(schema).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
tap(() => {
|
||||
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
||||
return response;
|
||||
}
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
map((response: MetadataschemaSuccessResponse) => {
|
||||
if (isNotEmpty(response.metadataschema)) {
|
||||
return response.metadataschema;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -448,74 +229,32 @@ export class RegistryService {
|
||||
* @param id The id of the metadata schema to delete
|
||||
*/
|
||||
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
||||
return this.delete(this.metadataSchemasPath, id);
|
||||
return this.metadataSchemaService.deleteAndReturnResponse(`${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that clears a cached metadata schema request and returns its REST url
|
||||
*/
|
||||
public clearMetadataSchemaRequests(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||
);
|
||||
return this.metadataSchemaService.clearRequests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a MetadataField
|
||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
||||
* - On creation, a CreateMetadataFieldRequest is used
|
||||
* - On update, a UpdateMetadataFieldRequest is used
|
||||
* - On creation, a CreateRequest is used
|
||||
* - On update, a PutRequest is used
|
||||
* @param field The MetadataField to create or update
|
||||
*/
|
||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||
const isUpdate = hasValue(field.id);
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => {
|
||||
if (isUpdate) {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'application/json');
|
||||
options.headers = headers;
|
||||
return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options);
|
||||
} else {
|
||||
return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post/put request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
// Return created/updated field
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: RestResponse) => {
|
||||
if (!response.isSuccessful) {
|
||||
if (hasValue((response as any).errorMessage)) {
|
||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
||||
}
|
||||
} else {
|
||||
return this.metadataFieldService.createOrUpdateMetadataField(field).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
tap(() => {
|
||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
||||
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
||||
return response;
|
||||
}
|
||||
}),
|
||||
isNotEmptyOperator(),
|
||||
map((response: MetadatafieldSuccessResponse) => {
|
||||
if (isNotEmpty(response.metadatafield)) {
|
||||
return response.metadatafield;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -525,38 +264,13 @@ export class RegistryService {
|
||||
* @param id The id of the metadata field to delete
|
||||
*/
|
||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||
return this.delete(this.metadataFieldsPath, id);
|
||||
return this.metadataFieldService.deleteAndReturnResponse(`${id}`);
|
||||
}
|
||||
/**
|
||||
* Method that clears a cached metadata field request and returns its REST url
|
||||
*/
|
||||
public clearMetadataFieldRequests(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||
);
|
||||
}
|
||||
|
||||
private delete(path: string, id: number): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(path).pipe(
|
||||
isNotEmptyOperator(),
|
||||
map((endpoint: string) => `${endpoint}/${id}`),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => new DeleteRequest(requestId, endpoint))
|
||||
);
|
||||
|
||||
// Execute the delete request
|
||||
request$.pipe(
|
||||
configureRequest(this.requestService)
|
||||
).subscribe();
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
getResponseFromEntry()
|
||||
);
|
||||
return this.metadataFieldService.clearRequests();
|
||||
}
|
||||
|
||||
private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) {
|
||||
|
@@ -5,27 +5,27 @@ export enum ActionType {
|
||||
/**
|
||||
* Action of reading, viewing or downloading something
|
||||
*/
|
||||
READ = 0,
|
||||
READ = 'READ',
|
||||
|
||||
/**
|
||||
* Action of modifying something
|
||||
*/
|
||||
WRITE = 1,
|
||||
WRITE = 'WRITE',
|
||||
|
||||
/**
|
||||
* Action of deleting something
|
||||
*/
|
||||
DELETE = 2,
|
||||
DELETE = 'DELETE',
|
||||
|
||||
/**
|
||||
* Action of adding something to a container
|
||||
*/
|
||||
ADD = 3,
|
||||
ADD = 'ADD',
|
||||
|
||||
/**
|
||||
* Action of removing something from a container
|
||||
*/
|
||||
REMOVE = 4,
|
||||
REMOVE = 'REMOVE',
|
||||
|
||||
/**
|
||||
* Action of performing workflow step 1
|
||||
@@ -50,15 +50,20 @@ export enum ActionType {
|
||||
/**
|
||||
* Default Read policies for Bitstreams submitted to container
|
||||
*/
|
||||
DEFAULT_BITSTREAM_READ = 9,
|
||||
DEFAULT_BITSTREAM_READ = 'DEFAULT_BITSTREAM_READ',
|
||||
|
||||
/**
|
||||
* Default Read policies for Items submitted to container
|
||||
*/
|
||||
DEFAULT_ITEM_READ = 10,
|
||||
DEFAULT_ITEM_READ = 'DEFAULT_ITEM_READ',
|
||||
|
||||
/**
|
||||
* Administrative actions
|
||||
*/
|
||||
ADMIN = 11,
|
||||
ADMIN = 'ADMIN',
|
||||
|
||||
/**
|
||||
* Action of withdrawn reading
|
||||
*/
|
||||
WITHDRAWN_READ = 'WITHDRAWN_READ'
|
||||
}
|
25
src/app/core/resource-policy/models/policy-type.model.ts
Normal file
25
src/app/core/resource-policy/models/policy-type.model.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Enum representing the Policy Type of a Resource Policy
|
||||
*/
|
||||
export enum PolicyType {
|
||||
/**
|
||||
* A policy in place during the submission
|
||||
*/
|
||||
TYPE_SUBMISSION = 'TYPE_SUBMISSION',
|
||||
|
||||
/**
|
||||
* A policy in place during the approval workflow
|
||||
*/
|
||||
TYPE_WORKFLOW = 'TYPE_WORKFLOW',
|
||||
|
||||
/**
|
||||
* A policy that has been inherited from a container (the collection)
|
||||
*/
|
||||
TYPE_INHERITED = 'TYPE_INHERITED',
|
||||
|
||||
/**
|
||||
* A policy defined by the user during the submission or workflow phase
|
||||
*/
|
||||
TYPE_CUSTOM = 'TYPE_CUSTOM',
|
||||
|
||||
}
|
105
src/app/core/resource-policy/models/resource-policy.model.ts
Normal file
105
src/app/core/resource-policy/models/resource-policy.model.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { autoserialize, deserialize, deserializeAs } from 'cerialize';
|
||||
import { link, typedObject } from '../../cache/builders/build-decorators';
|
||||
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
|
||||
import { ActionType } from './action-type.model';
|
||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||
import { HALLink } from '../../shared/hal-link.model';
|
||||
import { RESOURCE_POLICY } from './resource-policy.resource-type';
|
||||
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { PolicyType } from './policy-type.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
import { GROUP } from '../../eperson/models/group.resource-type';
|
||||
import { Group } from '../../eperson/models/group.model';
|
||||
import { EPERSON } from '../../eperson/models/eperson.resource-type';
|
||||
import { EPerson } from '../../eperson/models/eperson.model';
|
||||
|
||||
/**
|
||||
* Model class for a Resource Policy
|
||||
*/
|
||||
@typedObject
|
||||
export class ResourcePolicy implements CacheableObject {
|
||||
static type = RESOURCE_POLICY;
|
||||
|
||||
/**
|
||||
* The identifier for this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name for this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The description for this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* The classification or this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
policyType: PolicyType;
|
||||
|
||||
/**
|
||||
* The action that is allowed by this Resource Policy
|
||||
*/
|
||||
@autoserialize
|
||||
action: ActionType;
|
||||
|
||||
/**
|
||||
* The first day of validity of the policy (format YYYY-MM-DD)
|
||||
*/
|
||||
@autoserialize
|
||||
startDate: string;
|
||||
|
||||
/**
|
||||
* The last day of validity of the policy (format YYYY-MM-DD)
|
||||
*/
|
||||
@autoserialize
|
||||
endDate: string;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* The universally unique identifier for this Resource Policy
|
||||
* This UUID is generated client-side and isn't used by the backend.
|
||||
* It is based on the ID, so it will be the same for each refresh.
|
||||
*/
|
||||
@deserializeAs(new IDToUUIDSerializer('resource-policy'), 'id')
|
||||
uuid: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this ResourcePolicy
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
eperson: HALLink,
|
||||
group: HALLink,
|
||||
self: HALLink,
|
||||
};
|
||||
|
||||
/**
|
||||
* The eperson linked by this resource policy
|
||||
* Will be undefined unless the version {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(EPERSON)
|
||||
eperson?: Observable<RemoteData<EPerson>>;
|
||||
|
||||
/**
|
||||
* The group linked by this resource policy
|
||||
* Will be undefined unless the version {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(GROUP)
|
||||
group?: Observable<RemoteData<Group>>;
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for ResourcePolicy
|
||||
@@ -6,4 +6,4 @@ import { ResourceType } from './resource-type';
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const RESOURCE_POLICY = new ResourceType('resourcePolicy');
|
||||
export const RESOURCE_POLICY = new ResourceType('resourcepolicy');
|
319
src/app/core/resource-policy/resource-policy.service.spec.ts
Normal file
319
src/app/core/resource-policy/resource-policy.service.spec.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ResourcePolicyService } from './resource-policy.service';
|
||||
import { PolicyType } from './models/policy-type.model';
|
||||
import { ActionType } from './models/action-type.model';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
describe('ResourcePolicyService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: ResourcePolicyService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
|
||||
const resourcePolicy: any = {
|
||||
id: '1',
|
||||
name: null,
|
||||
description: null,
|
||||
policyType: PolicyType.TYPE_SUBMISSION,
|
||||
action: ActionType.READ,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
type: 'resourcepolicy',
|
||||
uuid: 'resource-policy-1',
|
||||
_links: {
|
||||
eperson: {
|
||||
href: 'https://rest.api/rest/api/eperson'
|
||||
},
|
||||
group: {
|
||||
href: 'https://rest.api/rest/api/group'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/resourcepolicies/1'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const anotherResourcePolicy: any = {
|
||||
id: '2',
|
||||
name: null,
|
||||
description: null,
|
||||
policyType: PolicyType.TYPE_SUBMISSION,
|
||||
action: ActionType.WRITE,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
type: 'resourcepolicy',
|
||||
uuid: 'resource-policy-2',
|
||||
_links: {
|
||||
eperson: {
|
||||
href: 'https://rest.api/rest/api/eperson'
|
||||
},
|
||||
group: {
|
||||
href: 'https://rest.api/rest/api/group'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/resourcepolicies/1'
|
||||
},
|
||||
}
|
||||
};
|
||||
const endpointURL = `https://rest.api/rest/api/resourcepolicies`;
|
||||
const requestURL = `https://rest.api/rest/api/resourcepolicies/${resourcePolicy.id}`;
|
||||
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
const resourcePolicyId = '1';
|
||||
const epersonUUID = '8b39g7ya-5a4b-438b-9686-be1d5b4a1c5a';
|
||||
const groupUUID = '8b39g7ya-5a4b-36987-9686-be1d5b4a1c5a';
|
||||
const resourceUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a';
|
||||
|
||||
const pageInfo = new PageInfo();
|
||||
const array = [resourcePolicy, anotherResourcePolicy];
|
||||
const paginatedList = new PaginatedList(pageInfo, array);
|
||||
const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy);
|
||||
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', { a: endpointURL })
|
||||
});
|
||||
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: observableOf(responseCacheEntry),
|
||||
getByUUID: observableOf(responseCacheEntry),
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildSingle: hot('a|', {
|
||||
a: resourcePolicyRD
|
||||
}),
|
||||
buildList: hot('a|', {
|
||||
a: paginatedListRD
|
||||
}),
|
||||
});
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
|
||||
service = new ResourcePolicyService(
|
||||
requestService,
|
||||
rdbService,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
|
||||
spyOn((service as any).dataService, 'create').and.callThrough();
|
||||
spyOn((service as any).dataService, 'delete').and.callThrough();
|
||||
spyOn((service as any).dataService, 'update').and.callThrough();
|
||||
spyOn((service as any).dataService, 'findById').and.callThrough();
|
||||
spyOn((service as any).dataService, 'findByHref').and.callThrough();
|
||||
spyOn((service as any).dataService, 'searchBy').and.callThrough();
|
||||
spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(requestURL));
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should proxy the call to dataservice.create with eperson UUID', () => {
|
||||
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID));
|
||||
const params = [
|
||||
new RequestParam('resource', resourceUUID),
|
||||
new RequestParam('eperson', epersonUUID)
|
||||
];
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params);
|
||||
});
|
||||
|
||||
it('should proxy the call to dataservice.create with group UUID', () => {
|
||||
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, null, groupUUID));
|
||||
const params = [
|
||||
new RequestParam('resource', resourceUUID),
|
||||
new RequestParam('group', groupUUID)
|
||||
];
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.create).toHaveBeenCalledWith(resourcePolicy, ...params);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ResourcePolicy> for the object with the given id', () => {
|
||||
const result = service.create(resourcePolicy, resourceUUID, epersonUUID);
|
||||
const expected = cold('a|', {
|
||||
a: resourcePolicyRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should proxy the call to dataservice.create', () => {
|
||||
scheduler.schedule(() => service.delete(resourcePolicyId));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.delete).toHaveBeenCalledWith(resourcePolicyId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should proxy the call to dataservice.update', () => {
|
||||
scheduler.schedule(() => service.update(resourcePolicy));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.update).toHaveBeenCalledWith(resourcePolicy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should proxy the call to dataservice.findById', () => {
|
||||
scheduler.schedule(() => service.findById(resourcePolicyId));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.findById).toHaveBeenCalledWith(resourcePolicyId);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ResourcePolicy> for the object with the given id', () => {
|
||||
const result = service.findById(resourcePolicyId);
|
||||
const expected = cold('a|', {
|
||||
a: resourcePolicyRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByHref', () => {
|
||||
it('should proxy the call to dataservice.findByHref', () => {
|
||||
scheduler.schedule(() => service.findByHref(requestURL));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<ResourcePolicy> for the object with the given URL', () => {
|
||||
const result = service.findByHref(requestURL);
|
||||
const expected = cold('a|', {
|
||||
a: resourcePolicyRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchByEPerson', () => {
|
||||
it('should proxy the call to dataservice.searchBy', () => {
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [new RequestParam('uuid', epersonUUID)];
|
||||
scheduler.schedule(() => service.searchByEPerson(epersonUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options);
|
||||
});
|
||||
|
||||
it('should proxy the call to dataservice.searchBy with additional search param', () => {
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [
|
||||
new RequestParam('uuid', epersonUUID),
|
||||
new RequestParam('resource', resourceUUID),
|
||||
];
|
||||
scheduler.schedule(() => service.searchByEPerson(epersonUUID, resourceUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByEPersonMethod, options);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
|
||||
const result = service.searchByEPerson(epersonUUID, resourceUUID);
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('searchByGroup', () => {
|
||||
it('should proxy the call to dataservice.searchBy', () => {
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [new RequestParam('uuid', groupUUID)];
|
||||
scheduler.schedule(() => service.searchByGroup(groupUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options);
|
||||
});
|
||||
|
||||
it('should proxy the call to dataservice.searchBy with additional search param', () => {
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [
|
||||
new RequestParam('uuid', groupUUID),
|
||||
new RequestParam('resource', resourceUUID),
|
||||
];
|
||||
scheduler.schedule(() => service.searchByGroup(groupUUID, resourceUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByGroupMethod, options);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
|
||||
const result = service.searchByGroup(groupUUID);
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('searchByResource', () => {
|
||||
it('should proxy the call to dataservice.searchBy', () => {
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [new RequestParam('uuid', resourceUUID)];
|
||||
scheduler.schedule(() => service.searchByResource(resourceUUID));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options);
|
||||
});
|
||||
|
||||
it('should proxy the call to dataservice.searchBy with additional search param', () => {
|
||||
const action = ActionType.READ;
|
||||
const options = new FindListOptions();
|
||||
options.searchParams = [
|
||||
new RequestParam('uuid', resourceUUID),
|
||||
new RequestParam('action', action),
|
||||
];
|
||||
scheduler.schedule(() => service.searchByResource(resourceUUID, action));
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.searchBy).toHaveBeenCalledWith((service as any).searchByResourceMethod, options);
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<ResourcePolicy>) for the search', () => {
|
||||
const result = service.searchByResource(resourceUUID);
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user