mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into w2p-73014_Metadata-edit-patch-error-fix
Conflicts: src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts
This commit is contained in:
21
.codecov.yml
Normal file
21
.codecov.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# DSpace configuration for Codecov.io coverage reports
|
||||||
|
# These override the default YAML settings at
|
||||||
|
# https://docs.codecov.io/docs/codecov-yaml#section-default-yaml
|
||||||
|
# Can be validated via instructions at:
|
||||||
|
# https://docs.codecov.io/docs/codecov-yaml#validate-your-repository-yaml
|
||||||
|
|
||||||
|
# Settings related to code coverage analysis
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
# For each PR, auto compare coverage to previous commit.
|
||||||
|
# Require that overall (project) coverage does NOT drop more than 0.5%
|
||||||
|
target: auto
|
||||||
|
threshold: 0.5%
|
||||||
|
|
||||||
|
# Turn PR comments "off". This feature adds the code coverage summary as a
|
||||||
|
# comment on each PR. See https://docs.codecov.io/docs/pull-request-comments
|
||||||
|
# However, this same info is available from the Codecov checks in the PR's
|
||||||
|
# "Checks" tab in GitHub. So, the comment is unnecessary.
|
||||||
|
comment: false
|
@@ -60,7 +60,7 @@ after_script:
|
|||||||
# Shutdown docker after everything runs
|
# Shutdown docker after everything runs
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||||
|
|
||||||
# After a successful build and test (see 'script'), send code coverage reports to coveralls.io
|
# After a successful build and test (see 'script'), send code coverage reports to codecov.io
|
||||||
# These code coverage reports are generated by the coveralls node module in our package.json
|
# These code coverage reports are generated by the codecov node module in our package.json
|
||||||
after_success:
|
after_success:
|
||||||
- cat coverage/dspace-angular/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
- codecov
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[](https://travis-ci.com/DSpace/dspace-angular) [](https://coveralls.io/github/DSpace/dspace-angular?branch=main) [](https://github.com/angular/universal)
|
[](https://travis-ci.com/DSpace/dspace-angular) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
||||||
|
|
||||||
dspace-angular
|
dspace-angular
|
||||||
==============
|
==============
|
||||||
|
@@ -97,6 +97,7 @@
|
|||||||
"json5": "^2.1.0",
|
"json5": "^2.1.0",
|
||||||
"jsonschema": "1.2.2",
|
"jsonschema": "1.2.2",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
|
"klaro": "^0.6.3",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^8.1.0",
|
"ng-mocks": "^8.1.0",
|
||||||
@@ -136,10 +137,10 @@
|
|||||||
"@types/js-cookie": "2.1.0",
|
"@types/js-cookie": "2.1.0",
|
||||||
"@types/lodash": "^4.14.110",
|
"@types/lodash": "^4.14.110",
|
||||||
"@types/node": "11.15.3",
|
"@types/node": "11.15.3",
|
||||||
|
"codecov": "^3.7.2",
|
||||||
"codelyzer": "^5.0.0",
|
"codelyzer": "^5.0.0",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
"compression-webpack-plugin": "^3.0.1",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"coveralls": "^3.0.0",
|
|
||||||
"css-loader": "3.4.0",
|
"css-loader": "3.4.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
|
72
server.ts
72
server.ts
@@ -15,7 +15,6 @@
|
|||||||
* import for `ngExpressEngine`.
|
* import for `ngExpressEngine`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'zone.js/dist/zone-node';
|
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'rxjs';
|
import 'rxjs';
|
||||||
|
|
||||||
@@ -34,6 +33,7 @@ import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
|
|||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
|
import { hasValue, hasNoValue } from './src/app/shared/empty.util';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -99,7 +99,6 @@ app.engine('html', (_, options, callback) =>
|
|||||||
/*
|
/*
|
||||||
* Register the view engines for html and ejs
|
* Register the view engines for html and ejs
|
||||||
*/
|
*/
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
app.set('view engine', 'html');
|
app.set('view engine', 'html');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -131,56 +130,31 @@ app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
|||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
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 (environment.universal.preboot) {
|
||||||
// If preboot is enabled, create a new zone for SSR, and
|
res.render(DIST_FOLDER + '/index.html', {
|
||||||
// register the error handler for when it throws an error
|
req,
|
||||||
Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => {
|
res,
|
||||||
res.render(DIST_FOLDER + '/index.html', {
|
preboot: environment.universal.preboot,
|
||||||
req,
|
async: environment.universal.async,
|
||||||
res,
|
time: environment.universal.time,
|
||||||
preboot: environment.universal.preboot,
|
baseUrl: environment.ui.nameSpace,
|
||||||
async: environment.universal.async,
|
originUrl: environment.ui.baseUrl,
|
||||||
time: environment.universal.time,
|
requestUrl: req.originalUrl
|
||||||
baseUrl: environment.ui.nameSpace,
|
}, (err, data) => {
|
||||||
originUrl: environment.ui.baseUrl,
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
requestUrl: req.originalUrl
|
res.send(data);
|
||||||
});
|
} else {
|
||||||
});
|
console.warn('Error in SSR, serving for direct CSR.');
|
||||||
|
if (hasValue(err)) {
|
||||||
|
console.warn('Error details : ', err);
|
||||||
|
}
|
||||||
|
res.sendFile(DIST_FOLDER + '/index.html');
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// If preboot is disabled, just serve the client side ejs template and pass it the required
|
// If preboot is disabled, just serve the client
|
||||||
// variables
|
|
||||||
console.log('Universal off, serving for direct CSR');
|
console.log('Universal off, serving for direct CSR');
|
||||||
res.render('index-csr.ejs', {
|
res.sendFile(DIST_FOLDER + '/index.html');
|
||||||
root: DIST_FOLDER,
|
|
||||||
scripts: `<script>window.dspace = ${JSON.stringify(dspace)}</script>`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
||||||
|
constructor(protected resolver: CollectionPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import {
|
|||||||
COLLECTION_EDIT_PATH,
|
COLLECTION_EDIT_PATH,
|
||||||
COLLECTION_CREATE_PATH
|
COLLECTION_CREATE_PATH
|
||||||
} from './collection-page-routing-paths';
|
} from './collection-page-routing-paths';
|
||||||
|
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,7 +40,7 @@ import {
|
|||||||
{
|
{
|
||||||
path: COLLECTION_EDIT_PATH,
|
path: COLLECTION_EDIT_PATH,
|
||||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [CollectionPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
@@ -78,7 +79,8 @@ import {
|
|||||||
CollectionBreadcrumbResolver,
|
CollectionBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCollectionPageGuard
|
CreateCollectionPageGuard,
|
||||||
|
CollectionPageAdministratorGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageRoutingModule {
|
export class CollectionPageRoutingModule {
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Community } from '../core/shared/community.model';
|
||||||
|
import { CommunityPageResolver } from './community-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
||||||
|
constructor(protected resolver: CommunityPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
|
|||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
||||||
|
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -31,7 +32,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
|||||||
{
|
{
|
||||||
path: COMMUNITY_EDIT_PATH,
|
path: COMMUNITY_EDIT_PATH,
|
||||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [CommunityPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
@@ -53,7 +54,8 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
|||||||
CommunityBreadcrumbResolver,
|
CommunityBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCommunityPageGuard
|
CreateCommunityPageGuard,
|
||||||
|
CommunityPageAdministratorGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageRoutingModule {
|
export class CommunityPageRoutingModule {
|
||||||
|
@@ -123,7 +123,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
/**
|
/**
|
||||||
* Check if the current page is entirely valid
|
* Check if the current page is entirely valid
|
||||||
*/
|
*/
|
||||||
protected isValid() {
|
public isValid() {
|
||||||
return this.objectUpdatesService.isValidPage(this.url);
|
return this.objectUpdatesService.isValidPage(this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,6 +29,8 @@ import {
|
|||||||
ITEM_EDIT_REINSTATE_PATH,
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
ITEM_EDIT_WITHDRAW_PATH
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
} from './edit-item-page.routing-paths';
|
} from './edit-item-page.routing-paths';
|
||||||
|
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||||
|
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -98,10 +100,12 @@ import {
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemWithdrawComponent,
|
component: ItemWithdrawComponent,
|
||||||
|
canActivate: [ItemPageWithdrawGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_REINSTATE_PATH,
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
component: ItemReinstateComponent,
|
component: ItemReinstateComponent,
|
||||||
|
canActivate: [ItemPageReinstateGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_PRIVATE_PATH,
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
@@ -154,7 +158,9 @@ import {
|
|||||||
I18nBreadcrumbResolver,
|
I18nBreadcrumbResolver,
|
||||||
I18nBreadcrumbsService,
|
I18nBreadcrumbsService,
|
||||||
ResourcePolicyResolver,
|
ResourcePolicyResolver,
|
||||||
ResourcePolicyTargetResolver
|
ResourcePolicyTargetResolver,
|
||||||
|
ItemPageReinstateGuard,
|
||||||
|
ItemPageWithdrawGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
@@ -5,14 +5,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="(editable | async)" class="field-container">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
[(ngModel)]="metadata.key"
|
[(ngModel)]="metadata.key"
|
||||||
|
[url]="this.url"
|
||||||
|
[metadata]="this.metadata"
|
||||||
(submitSuggestion)="update(suggestionControl)"
|
(submitSuggestion)="update(suggestionControl)"
|
||||||
(clickSuggestion)="update(suggestionControl)"
|
(clickSuggestion)="update(suggestionControl)"
|
||||||
(typeSuggestion)="update(suggestionControl)"
|
(typeSuggestion)="update(suggestionControl)"
|
||||||
(dsClickOutside)="checkValidity(suggestionControl)"
|
(dsClickOutside)="checkValidity(suggestionControl)"
|
||||||
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
(findSuggestions)="findMetadataFieldSuggestions($event)"
|
||||||
#suggestionControl="ngModel"
|
#suggestionControl="ngModel"
|
||||||
[dsInListValidator]="metadataFields"
|
|
||||||
[valid]="(valid | async) !== false"
|
[valid]="(valid | async) !== false"
|
||||||
dsAutoFocus autoFocusSelector=".suggestion_input"
|
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||||
[ngModelOptions]="{standalone: true}"
|
[ngModelOptions]="{standalone: true}"
|
||||||
@@ -46,12 +47,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)"
|
<button [disabled]="!(canSetEditable() | async)" *ngIf="!(editable | async)"
|
||||||
(click)="setEditable(true)" class="btn btn-outline-primary btn-sm"
|
(click)="setEditable(true)" class="btn btn-outline-primary btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.edit' | translate}}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!(canSetUneditable() | async)" *ngIf="(editable | async)"
|
<button [disabled]="!(canSetUneditable() | async) || (valid | async) === false" *ngIf="(editable | async)"
|
||||||
(click)="setEditable(false)" class="btn btn-outline-success btn-sm"
|
(click)="setEditable(false)" class="btn btn-outline-success btn-sm"
|
||||||
title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
|
title="{{'item.edit.metadata.edit.buttons.unedit' | translate}}">
|
||||||
<i class="fas fa-check fa-fw"></i>
|
<i class="fas fa-check fa-fw"></i>
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { MetadataFieldDataService } from '../../../../core/data/metadata-field-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
@@ -14,9 +15,14 @@ import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'
|
|||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
import { SharedModule } from '../../../../shared/shared.module';
|
import {
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../../../shared/remote-data.utils';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
||||||
|
import { FilterInputSuggestionsComponent } from '../../../../shared/input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||||
|
import { MockComponent, MockDirective } from 'ng-mocks';
|
||||||
|
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||||
|
|
||||||
let comp: EditInPlaceFieldComponent;
|
let comp: EditInPlaceFieldComponent;
|
||||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||||
@@ -25,17 +31,21 @@ let el: HTMLElement;
|
|||||||
let metadataFieldService;
|
let metadataFieldService;
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let paginatedMetadataFields;
|
let paginatedMetadataFields;
|
||||||
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' })
|
const mdSchema = Object.assign(new MetadataSchema(), { prefix: 'dc' });
|
||||||
|
const mdSchemaRD$ = createSuccessfulRemoteDataObject$(mdSchema);
|
||||||
const mdField1 = Object.assign(new MetadataField(), {
|
const mdField1 = Object.assign(new MetadataField(), {
|
||||||
schema: mdSchema,
|
schema: mdSchemaRD$,
|
||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'author'
|
qualifier: 'author'
|
||||||
});
|
});
|
||||||
const mdField2 = Object.assign(new MetadataField(), { schema: mdSchema, element: 'title' });
|
const mdField2 = Object.assign(new MetadataField(), {
|
||||||
|
schema: mdSchemaRD$,
|
||||||
|
element: 'title'
|
||||||
|
});
|
||||||
const mdField3 = Object.assign(new MetadataField(), {
|
const mdField3 = Object.assign(new MetadataField(), {
|
||||||
schema: mdSchema,
|
schema: mdSchemaRD$,
|
||||||
element: 'description',
|
element: 'description',
|
||||||
qualifier: 'abstract'
|
qualifier: 'abstract',
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadatum = Object.assign(new MetadatumViewModel(), {
|
const metadatum = Object.assign(new MetadatumViewModel(), {
|
||||||
@@ -74,11 +84,16 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [FormsModule, SharedModule, TranslateModule.forRoot()],
|
imports: [FormsModule, TranslateModule.forRoot()],
|
||||||
declarations: [EditInPlaceFieldComponent],
|
declarations: [
|
||||||
|
EditInPlaceFieldComponent,
|
||||||
|
MockDirective(DebounceDirective),
|
||||||
|
MockComponent(FilterInputSuggestionsComponent)
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: metadataFieldService },
|
{ provide: RegistryService, useValue: metadataFieldService },
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: MetadataFieldDataService, useValue: {} }
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -94,13 +109,12 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.fieldUpdate = fieldUpdate;
|
comp.fieldUpdate = fieldUpdate;
|
||||||
comp.metadata = metadatum;
|
comp.metadata = metadatum;
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.update();
|
comp.update();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
it('it should call saveChangeFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
@@ -112,6 +126,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
const editable = false;
|
const editable = false;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.setEditable(editable);
|
comp.setEditable(editable);
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
|
it('it should call setEditableFieldUpdate on the objectUpdatesService with the correct url and uuid and false', () => {
|
||||||
@@ -121,7 +136,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('editable is true', () => {
|
describe('editable is true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain input fields or textareas', () => {
|
it('the div should contain input fields or textareas', () => {
|
||||||
@@ -133,7 +148,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('editable is false', () => {
|
describe('editable is false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain no input fields or textareas', () => {
|
it('the div should contain no input fields or textareas', () => {
|
||||||
@@ -145,7 +160,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('isValid is true', () => {
|
describe('isValid is true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.valid = observableOf(true);
|
objectUpdatesService.isValid.and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should not contain an error message', () => {
|
it('the div should not contain an error message', () => {
|
||||||
@@ -157,10 +172,10 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('isValid is false', () => {
|
describe('isValid is false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.valid = observableOf(false);
|
objectUpdatesService.isValid.and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('the div should contain no input fields or textareas', () => {
|
it('there should be an error message', () => {
|
||||||
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
const errorMessages = de.queryAll(By.css('small.text-danger'));
|
||||||
expect(errorMessages.length).toBeGreaterThan(0);
|
expect(errorMessages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
@@ -170,6 +185,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.remove();
|
comp.remove();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
it('it should call saveRemoveFieldUpdate on the objectUpdatesService with the correct url and metadata', () => {
|
||||||
@@ -180,6 +196,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('removeChangesFromField', () => {
|
describe('removeChangesFromField', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.removeChangesFromField();
|
comp.removeChangesFromField();
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
|
it('it should call removeChangesFromField on the objectUpdatesService with the correct url and uuid', () => {
|
||||||
@@ -192,19 +209,19 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
const metadataFieldSuggestions: InputSuggestion[] =
|
const metadataFieldSuggestions: InputSuggestion[] =
|
||||||
[
|
[
|
||||||
{ displayValue: mdField1.toString().split('.').join('.​'), value: mdField1.toString() },
|
{ displayValue: ('dc.' + mdField1.toString()).split('.').join('.​'), value: ('dc.' + mdField1.toString()) },
|
||||||
{ displayValue: mdField2.toString().split('.').join('.​'), value: mdField2.toString() },
|
{ displayValue: ('dc.' + mdField2.toString()).split('.').join('.​'), value: ('dc.' + mdField2.toString()) },
|
||||||
{ displayValue: mdField3.toString().split('.').join('.​'), value: mdField3.toString() }
|
{ displayValue: ('dc.' + mdField3.toString()).split('.').join('.​'), value: ('dc.' + mdField3.toString()) }
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
comp.findMetadataFieldSuggestions(query);
|
comp.findMetadataFieldSuggestions(query);
|
||||||
|
tick();
|
||||||
});
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
|
it('it should call queryMetadataFields on the metadataFieldService with the correct query', () => {
|
||||||
|
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query, null, followLink('schema'));
|
||||||
expect(metadataFieldService.queryMetadataFields).toHaveBeenCalledWith(query);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should set metadataFieldSuggestions to the right value', () => {
|
it('it should set metadataFieldSuggestions to the right value', () => {
|
||||||
@@ -216,7 +233,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('canSetEditable', () => {
|
describe('canSetEditable', () => {
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetEditable should return an observable emitting false', () => {
|
it('canSetEditable should return an observable emitting false', () => {
|
||||||
@@ -227,12 +245,14 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently false', () => {
|
describe('when editable is currently false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently not REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canSetEditable should return an observable emitting true', () => {
|
it('canSetEditable should return an observable emitting true', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -243,6 +263,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canSetEditable should return an observable emitting false', () => {
|
it('canSetEditable should return an observable emitting false', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -255,7 +276,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('canSetUneditable', () => {
|
describe('canSetUneditable', () => {
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetUneditable should return an observable emitting true', () => {
|
it('canSetUneditable should return an observable emitting true', () => {
|
||||||
@@ -266,7 +288,8 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently false', () => {
|
describe('when editable is currently false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canSetUneditable should return an observable emitting false', () => {
|
it('canSetUneditable should return an observable emitting false', () => {
|
||||||
@@ -278,7 +301,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetEditable emits true', () => {
|
describe('when canSetEditable emits true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -290,7 +313,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetEditable emits false', () => {
|
describe('when canSetEditable emits false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(false);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(false));
|
||||||
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -302,7 +325,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetUneditable emits true', () => {
|
describe('when canSetUneditable emits true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -314,7 +337,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when canSetUneditable emits false', () => {
|
describe('when canSetUneditable emits false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -372,6 +395,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
|
describe('when the fieldUpdate\'s changeType is currently not REMOVE or ADD', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canRemove should return an observable emitting true', () => {
|
it('canRemove should return an observable emitting true', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -382,6 +406,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
describe('when the fieldUpdate\'s changeType is currently ADD', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canRemove should return an observable emitting false', () => {
|
it('canRemove should return an observable emitting false', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
@@ -394,7 +419,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
|
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
objectUpdatesService.isEditable.and.returnValue(observableOf(true));
|
||||||
comp.fieldUpdate.changeType = undefined;
|
comp.fieldUpdate.changeType = undefined;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -408,6 +433,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
|
describe('when the fieldUpdate\'s changeType is currently ADD, UPDATE or REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canUndo should return an observable emitting true', () => {
|
it('canUndo should return an observable emitting true', () => {
|
||||||
@@ -419,6 +445,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
|
describe('when the fieldUpdate\'s changeType is currently undefined', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.fieldUpdate.changeType = undefined;
|
comp.fieldUpdate.changeType = undefined;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('canUndo should return an observable emitting false', () => {
|
it('canUndo should return an observable emitting false', () => {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
|
import { metadataFieldsToString } from '../../../../core/shared/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@@ -9,8 +10,8 @@ import { FieldUpdate } from '../../../../core/data/object-updates/object-updates
|
|||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { NgModel } from '@angular/forms';
|
import { NgModel } from '@angular/forms';
|
||||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
@@ -32,15 +33,10 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() url: string;
|
@Input() url: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of strings with all metadata field keys available
|
|
||||||
*/
|
|
||||||
@Input() metadataFields: string[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The metadatum of this field
|
* The metadatum of this field
|
||||||
*/
|
*/
|
||||||
metadata: MetadatumViewModel;
|
@Input() metadata: MetadatumViewModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits whether or not this field is currently editable
|
* Emits whether or not this field is currently editable
|
||||||
@@ -126,27 +122,34 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Ignores fields from metadata schemas "relation" and "relationship"
|
* Ignores fields from metadata schemas "relation" and "relationship"
|
||||||
* @param query The query to look for
|
* @param query The query to look for
|
||||||
*/
|
*/
|
||||||
findMetadataFieldSuggestions(query: string): void {
|
findMetadataFieldSuggestions(query: string) {
|
||||||
if (isNotEmpty(query)) {
|
if (isNotEmpty(query)) {
|
||||||
this.registryService.queryMetadataFields(query).pipe(
|
return this.registryService.queryMetadataFields(query, null, followLink('schema')).pipe(
|
||||||
// getSucceededRemoteData(),
|
metadataFieldsToString(),
|
||||||
take(1),
|
take(1))
|
||||||
map((data) => data.payload.page)
|
.subscribe((fieldNames: string[]) => {
|
||||||
).subscribe(
|
this.setInputSuggestions(fieldNames);
|
||||||
(fields: MetadataField[]) => this.metadataFieldSuggestions.next(
|
})
|
||||||
fields.map((field: MetadataField) => {
|
|
||||||
return {
|
|
||||||
displayValue: field.toString().split('.').join('.​'),
|
|
||||||
value: field.toString()
|
|
||||||
};
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.metadataFieldSuggestions.next([]);
|
this.metadataFieldSuggestions.next([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the list of input suggestion with the given Metadata fields, which all require a resolved MetadataSchema
|
||||||
|
* @param fields list of Metadata fields, which all require a resolved MetadataSchema
|
||||||
|
*/
|
||||||
|
setInputSuggestions(fields: string[]) {
|
||||||
|
this.metadataFieldSuggestions.next(
|
||||||
|
fields.map((fieldName: string) => {
|
||||||
|
return {
|
||||||
|
displayValue: fieldName.split('.').join('.​'),
|
||||||
|
value: fieldName
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a user should be allowed to edit this field
|
* Check if a user should be allowed to edit this field
|
||||||
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
* @return an observable that emits true when the user should be able to edit this field and false when they should not
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||||
ds-edit-in-place-field
|
ds-edit-in-place-field
|
||||||
[fieldUpdate]="updateValue || {}"
|
[fieldUpdate]="updateValue || {}"
|
||||||
[metadataFields]="metadataFields$ | async"
|
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'table-warning': updateValue.changeType === 0,
|
'table-warning': updateValue.changeType === 0,
|
||||||
|
@@ -4,16 +4,13 @@ import { ItemDataService } from '../../../core/data/item-data.service';
|
|||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { first, switchMap, tap } from 'rxjs/operators';
|
||||||
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
|
||||||
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
|
||||||
import { UpdateDataService } from '../../../core/data/update-data.service';
|
import { UpdateDataService } from '../../../core/data/update-data.service';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
@@ -43,11 +40,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() updateService: UpdateDataService<Item>;
|
@Input() updateService: UpdateDataService<Item>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Observable with a list of strings with all existing metadata field keys
|
|
||||||
*/
|
|
||||||
metadataFields$: Observable<string[]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public itemService: ItemDataService,
|
public itemService: ItemDataService,
|
||||||
public objectUpdatesService: ObjectUpdatesService,
|
public objectUpdatesService: ObjectUpdatesService,
|
||||||
@@ -55,7 +47,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
public notificationsService: NotificationsService,
|
public notificationsService: NotificationsService,
|
||||||
public translateService: TranslateService,
|
public translateService: TranslateService,
|
||||||
public route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
public metadataFieldService: RegistryService,
|
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||||
}
|
}
|
||||||
@@ -65,7 +56,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.metadataFields$ = this.findMetadataFields();
|
|
||||||
if (hasNoValue(this.updateService)) {
|
if (hasNoValue(this.updateService)) {
|
||||||
this.updateService = this.itemService;
|
this.updateService = this.itemService;
|
||||||
}
|
}
|
||||||
@@ -139,16 +129,6 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to request all metadata fields and convert them to a list of strings
|
|
||||||
*/
|
|
||||||
findMetadataFields(): Observable<string[]> {
|
|
||||||
return this.metadataFieldService.getAllMetadataFields().pipe(
|
|
||||||
getSucceededRemoteData(),
|
|
||||||
take(1),
|
|
||||||
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
||||||
|
*/
|
||||||
|
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check reinstate authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.ReinstateItem);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
||||||
|
*/
|
||||||
|
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check withdraw authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.WithdrawItem);
|
||||||
|
}
|
||||||
|
}
|
@@ -15,7 +15,7 @@
|
|||||||
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)">{{getItemPage((itemRD$ | async)?.payload)}}</a>
|
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)">{{getItemPage((itemRD$ | async)?.payload)}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let operation of operations" class="w-100 pt-3">
|
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
||||||
<ds-item-operation [operation]="operation"></ds-item-operation>
|
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,6 +12,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
describe('ItemStatusComponent', () => {
|
describe('ItemStatusComponent', () => {
|
||||||
let comp: ItemStatusComponent;
|
let comp: ItemStatusComponent;
|
||||||
@@ -20,7 +21,10 @@ describe('ItemStatusComponent', () => {
|
|||||||
const mockItem = Object.assign(new Item(), {
|
const mockItem = Object.assign(new Item(), {
|
||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
handle: 'fake/handle',
|
handle: 'fake/handle',
|
||||||
lastModified: '2018'
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'test-item-selflink' }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemPageUrl = `items/${mockItem.id}`;
|
const itemPageUrl = `items/${mockItem.id}`;
|
||||||
@@ -31,13 +35,20 @@ describe('ItemStatusComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [ItemStatusComponent],
|
declarations: [ItemStatusComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@@ -3,15 +3,19 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-status',
|
selector: 'ds-item-status',
|
||||||
templateUrl: './item-status.component.html',
|
templateUrl: './item-status.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
animations: [
|
animations: [
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut
|
fadeInOut
|
||||||
@@ -40,14 +44,15 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
* The possible actions that can be performed on the item
|
* The possible actions that can be performed on the item
|
||||||
* key: id value: url to action's component
|
* key: id value: url to action's component
|
||||||
*/
|
*/
|
||||||
operations: ItemOperation[];
|
operations$: BehaviorSubject<ItemOperation[]> = new BehaviorSubject<ItemOperation[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The keys of the actions (to loop over)
|
* The keys of the actions (to loop over)
|
||||||
*/
|
*/
|
||||||
actionsKeys;
|
actionsKeys;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute,
|
||||||
|
private authorizationService: AuthorizationDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -67,21 +72,43 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
The value is supposed to be a href for the button
|
The value is supposed to be a href for the button
|
||||||
*/
|
*/
|
||||||
this.operations = [];
|
const operations = [];
|
||||||
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||||
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||||
if (item.isWithdrawn) {
|
operations.push(undefined);
|
||||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
// Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously
|
||||||
} else {
|
const indexOfWithdrawReinstate = operations.length - 1;
|
||||||
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw'));
|
|
||||||
}
|
|
||||||
if (item.isDiscoverable) {
|
if (item.isDiscoverable) {
|
||||||
this.operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
||||||
} else {
|
} else {
|
||||||
this.operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
||||||
|
}
|
||||||
|
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
||||||
|
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
||||||
|
|
||||||
|
this.operations$.next(operations);
|
||||||
|
|
||||||
|
if (item.isWithdrawn) {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.ReinstateItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
||||||
|
const newOperations = [...this.operations$.value];
|
||||||
|
if (authorized) {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate');
|
||||||
|
} else {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = undefined;
|
||||||
|
}
|
||||||
|
this.operations$.next(newOperations);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.WithdrawItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
||||||
|
const newOperations = [...this.operations$.value];
|
||||||
|
if (authorized) {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw');
|
||||||
|
} else {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = undefined;
|
||||||
|
}
|
||||||
|
this.operations$.next(newOperations);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
|
||||||
this.operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -102,4 +129,8 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
return getItemEditRoute(item.id);
|
return getItemEditRoute(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackOperation(index: number, operation: ItemOperation) {
|
||||||
|
return hasValue(operation) ? operation.operationKey : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
30
src/app/+item-page/item-page-administrator.guard.ts
Normal file
30
src/app/+item-page/item-page-administrator.guard.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,7 @@ import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.servi
|
|||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||||
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
||||||
|
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -34,7 +35,7 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_PATH,
|
path: ITEM_EDIT_PATH,
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [ItemPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: UPLOAD_BITSTREAM_PATH,
|
path: UPLOAD_BITSTREAM_PATH,
|
||||||
@@ -49,7 +50,8 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
|||||||
ItemPageResolver,
|
ItemPageResolver,
|
||||||
ItemBreadcrumbResolver,
|
ItemBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService
|
LinkService,
|
||||||
|
ItemPageAdministratorGuard
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@@ -60,3 +60,8 @@ export const UNAUTHORIZED_PATH = 'unauthorized';
|
|||||||
export function getUnauthorizedRoute() {
|
export function getUnauthorizedRoute() {
|
||||||
return `/${UNAUTHORIZED_PATH}`;
|
return `/${UNAUTHORIZED_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const INFO_MODULE_PATH = 'info';
|
||||||
|
export function getInfoModulePath() {
|
||||||
|
return `/${INFO_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||||
|
|
||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
@@ -12,55 +13,65 @@ import {
|
|||||||
REGISTER_PATH,
|
REGISTER_PATH,
|
||||||
PROFILE_MODULE_PATH,
|
PROFILE_MODULE_PATH,
|
||||||
ADMIN_MODULE_PATH,
|
ADMIN_MODULE_PATH,
|
||||||
BITSTREAM_MODULE_PATH
|
BITSTREAM_MODULE_PATH,
|
||||||
|
INFO_MODULE_PATH
|
||||||
} from './app-routing-paths';
|
} from './app-routing-paths';
|
||||||
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
||||||
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
|
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
|
||||||
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
||||||
|
import { ReloadGuard } from './core/reload/reload.guard';
|
||||||
|
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
|
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', canActivate: [AuthBlockingGuard],
|
||||||
{ path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' },
|
children: [
|
||||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
{ path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false }, canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule' },
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
|
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule', canActivate: [SiteRegisterGuard] },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
|
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule' },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{
|
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
path: 'mydspace',
|
{ path: BITSTREAM_MODULE_PATH, loadChildren: './+bitstream-page/bitstream-page.module#BitstreamPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
{
|
||||||
canActivate: [AuthenticatedGuard]
|
path: 'mydspace',
|
||||||
},
|
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
|
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard] },
|
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard, EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
path: 'workspaceitems',
|
{ path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
|
{
|
||||||
},
|
path: 'workspaceitems',
|
||||||
{
|
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule',
|
||||||
path: WORKFLOW_ITEM_MODULE_PATH,
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
|
},
|
||||||
},
|
{
|
||||||
{
|
path: WORKFLOW_ITEM_MODULE_PATH,
|
||||||
path: PROFILE_MODULE_PATH,
|
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule',
|
||||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
},
|
},
|
||||||
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
{
|
||||||
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
|
path: PROFILE_MODULE_PATH,
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
|
||||||
],
|
},
|
||||||
|
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] },
|
||||||
|
{ path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' },
|
||||||
|
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
|
||||||
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
|
]}
|
||||||
|
],
|
||||||
{
|
{
|
||||||
onSameUrlNavigation: 'reload',
|
onSameUrlNavigation: 'reload',
|
||||||
})
|
})
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="outer-wrapper">
|
<div class="outer-wrapper" *ngIf="isNotAuthBlocking$ | async; else authLoader">
|
||||||
<ds-admin-sidebar></ds-admin-sidebar>
|
<ds-admin-sidebar></ds-admin-sidebar>
|
||||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
||||||
@@ -23,3 +23,8 @@
|
|||||||
<ds-footer></ds-footer>
|
<ds-footer></ds-footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-template #authLoader>
|
||||||
|
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center">
|
||||||
|
<ds-loading [showMessage]="false"></ds-loading>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@@ -47,3 +47,7 @@ ds-admin-sidebar {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: $sidebar-z-index;
|
z-index: $sidebar-z-index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ds-full-screen-loader {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
|
import * as ngrx from '@ngrx/store';
|
||||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
@@ -32,11 +31,11 @@ import { RouterMock } from './shared/mocks/router.mock';
|
|||||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||||
import { storeModuleConfig } from './app.reducer';
|
import { storeModuleConfig } from './app.reducer';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
import { authReducer } from './core/auth/auth.reducer';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
let de: DebugElement;
|
|
||||||
let el: HTMLElement;
|
|
||||||
const menuService = new MenuServiceStub();
|
const menuService = new MenuServiceStub();
|
||||||
|
|
||||||
describe('App component', () => {
|
describe('App component', () => {
|
||||||
@@ -52,7 +51,7 @@ describe('App component', () => {
|
|||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
StoreModule.forRoot({}, storeModuleConfig),
|
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
@@ -82,12 +81,19 @@ describe('App component', () => {
|
|||||||
|
|
||||||
// synchronous beforeEach
|
// synchronous beforeEach
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(AppComponent);
|
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||||
|
return () => {
|
||||||
|
return () => cold('a', {
|
||||||
|
a: {
|
||||||
|
core: { auth: { loading: false } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
comp = fixture.componentInstance; // component test instance
|
comp = fixture.componentInstance; // component test instance
|
||||||
// query for the <div class='outer-wrapper'> by CSS element selector
|
fixture.detectChanges();
|
||||||
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
|
|
||||||
el = de.nativeElement;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create component', inject([AppComponent], (app: AppComponent) => {
|
it('should create component', inject([AppComponent], (app: AppComponent) => {
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { delay, filter, map, take } from 'rxjs/operators';
|
import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostListener,
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
OnInit,
|
OnInit, Optional,
|
||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||||
@@ -19,7 +19,7 @@ import { MetadataService } from './core/metadata/metadata.service';
|
|||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
import { HostWindowState } from './shared/search/host-window.reducer';
|
import { HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
import { isAuthenticated } from './core/auth/selectors';
|
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
@@ -31,8 +31,8 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
|||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
import { hasValue } from './shared/empty.util';
|
||||||
export const LANG_COOKIE = 'language_cookie';
|
import { KlaroService } from './shared/cookies/klaro.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -52,6 +52,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
notificationOptions = environment.notifications;
|
notificationOptions = environment.notifications;
|
||||||
models;
|
models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the authentication is currently blocking the UI
|
||||||
|
*/
|
||||||
|
isNotAuthBlocking$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
@@ -64,8 +69,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService
|
private localeService: LocaleService,
|
||||||
|
@Optional() private cookiesService: KlaroService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/* Use models object so all decorators are actually called */
|
/* Use models object so all decorators are actually called */
|
||||||
this.models = models;
|
this.models = models;
|
||||||
// Load all the languages that are defined as active from the config file
|
// Load all the languages that are defined as active from the config file
|
||||||
@@ -86,19 +93,25 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
console.info(environment);
|
console.info(environment);
|
||||||
}
|
}
|
||||||
this.storeCSSVariables();
|
this.storeCSSVariables();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.isNotAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||||
|
map((isBlocking: boolean) => isBlocking === false),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
this.isNotAuthBlocking$
|
||||||
|
.pipe(
|
||||||
|
filter((notBlocking: boolean) => notBlocking),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => this.initializeKlaro());
|
||||||
|
|
||||||
const env: string = environment.production ? 'Production' : 'Development';
|
const env: string = environment.production ? 'Production' : 'Development';
|
||||||
const color: string = environment.production ? 'red' : 'green';
|
const color: string = environment.production ? 'red' : 'green';
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||||
|
|
||||||
// Whether is not authenticathed try to retrieve a possible stored auth token
|
|
||||||
this.store.pipe(select(isAuthenticated),
|
|
||||||
take(1),
|
|
||||||
filter((authenticated) => !authenticated)
|
|
||||||
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
|
||||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||||
|
|
||||||
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
||||||
@@ -154,4 +167,9 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeKlaro() {
|
||||||
|
if (hasValue(this.cookiesService)) {
|
||||||
|
this.cookiesService.initialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -21,6 +21,7 @@ import { AppComponent } from './app.component';
|
|||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
||||||
import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
||||||
|
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||||
|
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
import { ClientCookieService } from './core/services/client-cookie.service';
|
||||||
@@ -91,6 +92,15 @@ const PROVIDERS = [
|
|||||||
useClass: DSpaceRouterStateSerializer
|
useClass: DSpaceRouterStateSerializer
|
||||||
},
|
},
|
||||||
ClientCookieService,
|
ClientCookieService,
|
||||||
|
// Check the authentication token when the app initializes
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: (store: Store<AppState>,) => {
|
||||||
|
return () => store.dispatch(new CheckAuthenticationTokenAction());
|
||||||
|
},
|
||||||
|
deps: [ Store ],
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
...DYNAMIC_MATCHER_PROVIDERS,
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -4,7 +4,6 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { CommunityListPageComponent } from './community-list-page.component';
|
import { CommunityListPageComponent } from './community-list-page.component';
|
||||||
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
||||||
import { CommunityListComponent } from './community-list/community-list.component';
|
import { CommunityListComponent } from './community-list/community-list.component';
|
||||||
import { CdkTreeModule } from '@angular/cdk/tree';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page which houses a title and the community list, as described in community-list.component
|
* The page which houses a title and the community list, as described in community-list.component
|
||||||
@@ -13,8 +12,7 @@ import { CdkTreeModule } from '@angular/cdk/tree';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CommunityListPageRoutingModule,
|
CommunityListPageRoutingModule
|
||||||
CdkTreeModule,
|
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CommunityListPageComponent,
|
CommunityListPageComponent,
|
||||||
|
62
src/app/core/auth/auth-blocking.guard.spec.ts
Normal file
62
src/app/core/auth/auth-blocking.guard.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import * as ngrx from '@ngrx/store';
|
||||||
|
import { cold, getTestScheduler, initTestScheduler, resetTestScheduler } from 'jasmine-marbles/es6';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { AuthBlockingGuard } from './auth-blocking.guard';
|
||||||
|
|
||||||
|
describe('AuthBlockingGuard', () => {
|
||||||
|
let guard: AuthBlockingGuard;
|
||||||
|
beforeEach(() => {
|
||||||
|
guard = new AuthBlockingGuard(new Store<AppState>(undefined, undefined, undefined));
|
||||||
|
initTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
getTestScheduler().flush();
|
||||||
|
resetTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`canActivate`, () => {
|
||||||
|
|
||||||
|
describe(`when authState.loading is undefined`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||||
|
return () => {
|
||||||
|
return () => observableOf(undefined);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
it(`should not emit anything`, () => {
|
||||||
|
expect(guard.canActivate()).toBeObservable(cold('|'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when authState.loading is true`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||||
|
return () => {
|
||||||
|
return () => observableOf(true);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
it(`should not emit anything`, () => {
|
||||||
|
expect(guard.canActivate()).toBeObservable(cold('|'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when authState.loading is false`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||||
|
return () => {
|
||||||
|
return () => observableOf(false);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
it(`should succeed`, () => {
|
||||||
|
expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
34
src/app/core/auth/auth-blocking.guard.ts
Normal file
34
src/app/core/auth/auth-blocking.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate } from '@angular/router';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, map, take } from 'rxjs/operators';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { isAuthenticationBlocking } from './selectors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard that blocks the loading of any
|
||||||
|
* route until the authentication status has loaded.
|
||||||
|
* To ensure all rest requests get the correct auth header.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthBlockingGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(private store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the authentication isn't blocking everything
|
||||||
|
*/
|
||||||
|
canActivate(): Observable<boolean> {
|
||||||
|
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||||
|
map((isBlocking: boolean) => isBlocking === false),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((finished: boolean) => finished === true),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -34,6 +34,7 @@ export const AuthActionTypes = {
|
|||||||
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
||||||
|
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -335,6 +336,20 @@ export class SetRedirectUrlAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start loading for a hard redirect
|
||||||
|
* @class StartHardRedirectLoadingAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class RedirectAfterLoginSuccessAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.payload = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the authenticated eperson.
|
* Retrieve the authenticated eperson.
|
||||||
* @class RetrieveAuthenticatedEpersonAction
|
* @class RetrieveAuthenticatedEpersonAction
|
||||||
@@ -402,8 +417,8 @@ export type AuthActions
|
|||||||
| RetrieveAuthMethodsSuccessAction
|
| RetrieveAuthMethodsSuccessAction
|
||||||
| RetrieveAuthMethodsErrorAction
|
| RetrieveAuthMethodsErrorAction
|
||||||
| RetrieveTokenAction
|
| RetrieveTokenAction
|
||||||
| ResetAuthenticationMessagesAction
|
|
||||||
| RetrieveAuthenticatedEpersonAction
|
| RetrieveAuthenticatedEpersonAction
|
||||||
| RetrieveAuthenticatedEpersonErrorAction
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
| RetrieveAuthenticatedEpersonSuccessAction
|
| RetrieveAuthenticatedEpersonSuccessAction
|
||||||
| SetRedirectUrlAction;
|
| SetRedirectUrlAction
|
||||||
|
| RedirectAfterLoginSuccessAction;
|
||||||
|
@@ -27,6 +27,7 @@ import {
|
|||||||
CheckAuthenticationTokenCookieAction,
|
CheckAuthenticationTokenCookieAction,
|
||||||
LogOutErrorAction,
|
LogOutErrorAction,
|
||||||
LogOutSuccessAction,
|
LogOutSuccessAction,
|
||||||
|
RedirectAfterLoginSuccessAction,
|
||||||
RefreshTokenAction,
|
RefreshTokenAction,
|
||||||
RefreshTokenErrorAction,
|
RefreshTokenErrorAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
@@ -79,7 +80,26 @@ export class AuthEffects {
|
|||||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||||
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
|
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
|
||||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe(
|
||||||
|
take(1),
|
||||||
|
map((redirectUrl: string) => [action, redirectUrl])
|
||||||
|
)),
|
||||||
|
map(([action, redirectUrl]: [AuthenticatedSuccessAction, string]) => {
|
||||||
|
if (hasValue(redirectUrl)) {
|
||||||
|
return new RedirectAfterLoginSuccessAction(redirectUrl);
|
||||||
|
} else {
|
||||||
|
return new RetrieveAuthenticatedEpersonAction(action.payload.userHref);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
@Effect({ dispatch: false })
|
||||||
|
public redirectAfterLoginSuccess$: Observable<Action> = this.actions$.pipe(
|
||||||
|
ofType(AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS),
|
||||||
|
tap((action: RedirectAfterLoginSuccessAction) => {
|
||||||
|
this.authService.clearRedirectUrl();
|
||||||
|
this.authService.navigateToRedirectUrl(action.payload);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// It means "reacts to this action but don't send another"
|
// It means "reacts to this action but don't send another"
|
||||||
@@ -201,13 +221,6 @@ export class AuthEffects {
|
|||||||
tap(() => this.authService.refreshAfterLogout())
|
tap(() => this.authService.refreshAfterLogout())
|
||||||
);
|
);
|
||||||
|
|
||||||
@Effect({ dispatch: false })
|
|
||||||
public redirectToLogin$: Observable<Action> = this.actions$
|
|
||||||
.pipe(ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED),
|
|
||||||
tap(() => this.authService.removeToken()),
|
|
||||||
tap(() => this.authService.redirectToLogin())
|
|
||||||
);
|
|
||||||
|
|
||||||
@Effect({ dispatch: false })
|
@Effect({ dispatch: false })
|
||||||
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
|
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@@ -251,7 +251,6 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
|
|
||||||
// Pass on the new request instead of the original request.
|
// Pass on the new request instead of the original request.
|
||||||
return next.handle(newReq).pipe(
|
return next.handle(newReq).pipe(
|
||||||
// tap((response) => console.log('next.handle: ', response)),
|
|
||||||
map((response) => {
|
map((response) => {
|
||||||
// Intercept a Login/Logout response
|
// Intercept a Login/Logout response
|
||||||
if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) {
|
if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) {
|
||||||
|
@@ -42,6 +42,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
const action = new AuthenticateAction('user', 'password');
|
const action = new AuthenticateAction('user', 'password');
|
||||||
@@ -49,6 +50,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
@@ -62,6 +64,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -76,6 +79,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -84,6 +88,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
@@ -96,6 +101,7 @@ describe('authReducer', () => {
|
|||||||
it('should properly set the state, in response to a AUTHENTICATED action', () => {
|
it('should properly set the state, in response to a AUTHENTICATED action', () => {
|
||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
|
blocking: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -103,8 +109,15 @@ describe('authReducer', () => {
|
|||||||
};
|
};
|
||||||
const action = new AuthenticatedAction(mockTokenInfo);
|
const action = new AuthenticatedAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
expect(newState).toEqual(initialState);
|
authenticated: false,
|
||||||
|
blocking: true,
|
||||||
|
loaded: false,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
info: undefined
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly set the state, in response to a AUTHENTICATED_SUCCESS action', () => {
|
it('should properly set the state, in response to a AUTHENTICATED_SUCCESS action', () => {
|
||||||
@@ -112,6 +125,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -122,6 +136,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -133,6 +148,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -143,6 +159,7 @@ describe('authReducer', () => {
|
|||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: 'Test error message',
|
error: 'Test error message',
|
||||||
loaded: true,
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -153,6 +170,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenAction();
|
const action = new CheckAuthenticationTokenAction();
|
||||||
@@ -160,6 +178,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -169,6 +188,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenCookieAction();
|
const action = new CheckAuthenticationTokenCookieAction();
|
||||||
@@ -176,6 +196,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -187,6 +208,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -204,6 +226,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -216,7 +239,8 @@ describe('authReducer', () => {
|
|||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
loading: false,
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined
|
||||||
@@ -230,6 +254,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -242,6 +267,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: 'Test error message',
|
error: 'Test error message',
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -255,6 +281,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -265,6 +292,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -277,6 +305,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -287,6 +316,7 @@ describe('authReducer', () => {
|
|||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: 'Test error message',
|
error: 'Test error message',
|
||||||
loaded: true,
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined
|
||||||
};
|
};
|
||||||
@@ -299,6 +329,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -311,6 +342,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
@@ -325,6 +357,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
@@ -338,6 +371,7 @@ describe('authReducer', () => {
|
|||||||
authToken: newTokenInfo,
|
authToken: newTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
@@ -352,6 +386,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
@@ -364,6 +399,7 @@ describe('authReducer', () => {
|
|||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
@@ -378,6 +414,7 @@ describe('authReducer', () => {
|
|||||||
authToken: mockTokenInfo,
|
authToken: mockTokenInfo,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id
|
||||||
@@ -387,6 +424,7 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: 'Message',
|
info: 'Message',
|
||||||
@@ -410,6 +448,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
const action = new AddAuthenticationMessageAction('Message');
|
const action = new AddAuthenticationMessageAction('Message');
|
||||||
@@ -417,6 +456,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: 'Message'
|
info: 'Message'
|
||||||
};
|
};
|
||||||
@@ -427,6 +467,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
info: 'Message'
|
info: 'Message'
|
||||||
@@ -436,6 +477,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: undefined
|
info: undefined
|
||||||
@@ -447,6 +489,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false
|
loading: false
|
||||||
};
|
};
|
||||||
const action = new SetRedirectUrlAction('redirect.url');
|
const action = new SetRedirectUrlAction('redirect.url');
|
||||||
@@ -454,6 +497,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
redirectUrl: 'redirect.url'
|
redirectUrl: 'redirect.url'
|
||||||
};
|
};
|
||||||
@@ -464,6 +508,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
@@ -472,6 +517,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
@@ -482,6 +528,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
@@ -494,6 +541,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: authMethods
|
authMethods: authMethods
|
||||||
};
|
};
|
||||||
@@ -504,6 +552,7 @@ describe('authReducer', () => {
|
|||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
@@ -513,6 +562,7 @@ describe('authReducer', () => {
|
|||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||||
};
|
};
|
||||||
|
@@ -39,6 +39,10 @@ export interface AuthState {
|
|||||||
// true when loading
|
// true when loading
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
||||||
|
// true when everything else should wait for authorization
|
||||||
|
// to complete
|
||||||
|
blocking: boolean;
|
||||||
|
|
||||||
// info message
|
// info message
|
||||||
info?: string;
|
info?: string;
|
||||||
|
|
||||||
@@ -62,6 +66,7 @@ export interface AuthState {
|
|||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: []
|
||||||
};
|
};
|
||||||
@@ -86,7 +91,8 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN:
|
||||||
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: true
|
loading: true,
|
||||||
|
blocking: true
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.AUTHENTICATED_ERROR:
|
case AuthActionTypes.AUTHENTICATED_ERROR:
|
||||||
@@ -96,6 +102,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: (action as AuthenticationErrorAction).payload.message,
|
error: (action as AuthenticationErrorAction).payload.message,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,6 +117,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
blocking: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload
|
||||||
});
|
});
|
||||||
@@ -119,6 +127,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: (action as AuthenticationErrorAction).payload.message,
|
error: (action as AuthenticationErrorAction).payload.message,
|
||||||
|
blocking: false,
|
||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,25 +141,39 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
error: (action as LogOutErrorAction).payload.message
|
error: (action as LogOutErrorAction).payload.message
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.LOG_OUT_SUCCESS:
|
|
||||||
case AuthActionTypes.REFRESH_TOKEN_ERROR:
|
case AuthActionTypes.REFRESH_TOKEN_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.LOG_OUT_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
authenticated: false,
|
||||||
|
authToken: undefined,
|
||||||
|
error: undefined,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
|
info: undefined,
|
||||||
|
refreshing: false,
|
||||||
|
userId: undefined
|
||||||
|
});
|
||||||
|
|
||||||
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED:
|
||||||
case AuthActionTypes.REDIRECT_TOKEN_EXPIRED:
|
case AuthActionTypes.REDIRECT_TOKEN_EXPIRED:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload,
|
||||||
userId: undefined
|
userId: undefined
|
||||||
@@ -181,18 +204,21 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
// next three cases are used by dynamic rendering of login methods
|
// next three cases are used by dynamic rendering of login methods
|
||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: true
|
loading: true,
|
||||||
|
blocking: true
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
blocking: false,
|
||||||
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
blocking: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,6 +227,12 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
redirectUrl: (action as SetRedirectUrlAction).payload,
|
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: true,
|
||||||
|
blocking: true,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ import { EPersonDataService } from '../eperson/eperson-data.service';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ describe('AuthService test', () => {
|
|||||||
let authenticatedState;
|
let authenticatedState;
|
||||||
let unAuthenticatedState;
|
let unAuthenticatedState;
|
||||||
let linkService;
|
let linkService;
|
||||||
|
let hardRedirectService;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
mockStore = jasmine.createSpyObj('store', {
|
mockStore = jasmine.createSpyObj('store', {
|
||||||
@@ -77,6 +79,7 @@ describe('AuthService test', () => {
|
|||||||
linkService = {
|
linkService = {
|
||||||
resolveLinks: {}
|
resolveLinks: {}
|
||||||
};
|
};
|
||||||
|
hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']);
|
||||||
spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) });
|
spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) });
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -104,6 +107,7 @@ describe('AuthService test', () => {
|
|||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: Store, useValue: mockStore },
|
{ provide: Store, useValue: mockStore },
|
||||||
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||||
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
CookieService,
|
CookieService,
|
||||||
AuthService
|
AuthService
|
||||||
],
|
],
|
||||||
@@ -210,7 +214,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true when user is logged in', () => {
|
it('should return true when user is logged in', () => {
|
||||||
@@ -289,7 +293,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||||
storage = (authService as any).storage;
|
storage = (authService as any).storage;
|
||||||
routeServiceMock = TestBed.get(RouteService);
|
routeServiceMock = TestBed.get(RouteService);
|
||||||
routerStub = TestBed.get(Router);
|
routerStub = TestBed.get(Router);
|
||||||
@@ -318,36 +322,28 @@ describe('AuthService test', () => {
|
|||||||
expect(storage.remove).toHaveBeenCalled();
|
expect(storage.remove).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set redirect url to previous page', () => {
|
it('should redirect to reload with redirect url', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
authService.navigateToRedirectUrl('/collection/123');
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
// Reload with redirect URL set to /collection/123
|
||||||
authService.redirectAfterLoginSuccess(true);
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123'))));
|
||||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
|
||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set redirect url to current page', () => {
|
it('should redirect to reload with /home', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
authService.navigateToRedirectUrl('/home');
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
// Reload with redirect URL set to /home
|
||||||
authService.redirectAfterLoginSuccess(false);
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home'))));
|
||||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
|
||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to / and not to /login', () => {
|
it('should redirect to regular reload and not to /login', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
|
authService.navigateToRedirectUrl('/login');
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
// Reload without a redirect URL
|
||||||
authService.redirectAfterLoginSuccess(true);
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
|
||||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
|
||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to / when no redirect url is found', () => {
|
it('should redirect to regular reload when no redirect url is found', () => {
|
||||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
|
authService.navigateToRedirectUrl(undefined);
|
||||||
spyOn(routerStub, 'navigateByUrl');
|
// Reload without a redirect URL
|
||||||
authService.redirectAfterLoginSuccess(true);
|
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
|
||||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
|
||||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('impersonate', () => {
|
describe('impersonate', () => {
|
||||||
@@ -464,6 +460,14 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('refreshAfterLogout', () => {
|
||||||
|
it('should call navigateToRedirectUrl with no url', () => {
|
||||||
|
spyOn(authService as any, 'navigateToRedirectUrl').and.stub();
|
||||||
|
authService.refreshAfterLogout();
|
||||||
|
expect((authService as any).navigateToRedirectUrl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user is not logged in', () => {
|
describe('when user is not logged in', () => {
|
||||||
@@ -496,7 +500,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = unAuthenticatedState;
|
(state as any).core.auth = unAuthenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return null for the shortlived token', () => {
|
it('should return null for the shortlived token', () => {
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
import { Inject, Injectable, Optional } from '@angular/core';
|
import { Inject, Injectable, Optional } from '@angular/core';
|
||||||
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
import { map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { RouterReducerState } from '@ngrx/router-store';
|
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
@@ -14,7 +13,15 @@ import { AuthRequestService } from './auth-request.service';
|
|||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
import {
|
||||||
|
hasValue,
|
||||||
|
hasValueOperator,
|
||||||
|
isEmpty,
|
||||||
|
isNotEmpty,
|
||||||
|
isNotNull,
|
||||||
|
isNotUndefined,
|
||||||
|
hasNoValue
|
||||||
|
} from '../../shared/empty.util';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import {
|
import {
|
||||||
getAuthenticatedUserId,
|
getAuthenticatedUserId,
|
||||||
@@ -24,7 +31,7 @@ import {
|
|||||||
isTokenRefreshing,
|
isTokenRefreshing,
|
||||||
isAuthenticatedLoaded
|
isAuthenticatedLoaded
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction,
|
||||||
@@ -36,6 +43,7 @@ import { RouteService } from '../services/route.service';
|
|||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
@@ -62,43 +70,13 @@ export class AuthService {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected storage: CookieService,
|
protected storage: CookieService,
|
||||||
protected store: Store<AppState>
|
protected store: Store<AppState>,
|
||||||
|
protected hardRedirectService: HardRedirectService
|
||||||
) {
|
) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
startWith(false)
|
startWith(false)
|
||||||
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
|
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
|
||||||
|
|
||||||
// If current route is different from the one setted in authentication guard
|
|
||||||
// and is not the login route, clear redirect url and messages
|
|
||||||
const routeUrl$ = this.store.pipe(
|
|
||||||
select(routerStateSelector),
|
|
||||||
filter((routerState: RouterReducerState) => isNotUndefined(routerState)
|
|
||||||
&& isNotUndefined(routerState.state) && isNotEmpty(routerState.state.url)),
|
|
||||||
filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)),
|
|
||||||
map((routerState: RouterReducerState) => routerState.state.url)
|
|
||||||
);
|
|
||||||
const redirectUrl$ = this.store.pipe(select(getRedirectUrl), distinctUntilChanged());
|
|
||||||
routeUrl$.pipe(
|
|
||||||
withLatestFrom(redirectUrl$),
|
|
||||||
map(([routeUrl, redirectUrl]) => [routeUrl, redirectUrl])
|
|
||||||
).pipe(filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl)))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.clearRedirectUrl();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if is a login page route
|
|
||||||
*
|
|
||||||
* @param {string} url
|
|
||||||
* @returns {Boolean}.
|
|
||||||
*/
|
|
||||||
protected isLoginRoute(url: string) {
|
|
||||||
const urlTree: UrlTree = this.router.parseUrl(url);
|
|
||||||
const g: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
|
||||||
const segment = '/' + g.toString();
|
|
||||||
return segment === LOGIN_ROUTE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,69 +387,38 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the route navigated before the login
|
* Perform a hard redirect to the URL
|
||||||
|
* @param redirectUrl
|
||||||
*/
|
*/
|
||||||
public redirectAfterLoginSuccess(isStandalonePage: boolean) {
|
public navigateToRedirectUrl(redirectUrl: string) {
|
||||||
this.getRedirectUrl().pipe(
|
let url = `/reload/${new Date().getTime()}`;
|
||||||
take(1))
|
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
||||||
.subscribe((redirectUrl) => {
|
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
||||||
|
|
||||||
if (isNotEmpty(redirectUrl)) {
|
|
||||||
this.clearRedirectUrl();
|
|
||||||
this.router.onSameUrlNavigation = 'reload';
|
|
||||||
this.navigateToRedirectUrl(redirectUrl);
|
|
||||||
} else {
|
|
||||||
// If redirectUrl is empty use history.
|
|
||||||
this.routeService.getHistory().pipe(
|
|
||||||
take(1)
|
|
||||||
).subscribe((history) => {
|
|
||||||
let redirUrl;
|
|
||||||
if (isStandalonePage) {
|
|
||||||
// For standalone login pages, use the previous route.
|
|
||||||
redirUrl = history[history.length - 2] || '';
|
|
||||||
} else {
|
|
||||||
redirUrl = history[history.length - 1] || '';
|
|
||||||
}
|
|
||||||
this.navigateToRedirectUrl(redirUrl);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected navigateToRedirectUrl(redirectUrl: string) {
|
|
||||||
const url = decodeURIComponent(redirectUrl);
|
|
||||||
// in case the user navigates directly to /login (via bookmark, etc), or the route history is not found.
|
|
||||||
if (isEmpty(url) || url.startsWith(LOGIN_ROUTE)) {
|
|
||||||
this.router.navigateByUrl('/');
|
|
||||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
|
||||||
// this._window.nativeWindow.location.href = '/';
|
|
||||||
} else {
|
|
||||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
|
||||||
// this._window.nativeWindow.location.href = url;
|
|
||||||
this.router.navigateByUrl(url);
|
|
||||||
}
|
}
|
||||||
|
this.hardRedirectService.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh route navigated
|
* Refresh route navigated
|
||||||
*/
|
*/
|
||||||
public refreshAfterLogout() {
|
public refreshAfterLogout() {
|
||||||
// Hard redirect to the reload page with a unique number behind it
|
this.navigateToRedirectUrl(undefined);
|
||||||
// so that all state is definitely lost
|
|
||||||
this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get redirect url
|
* Get redirect url
|
||||||
*/
|
*/
|
||||||
getRedirectUrl(): Observable<string> {
|
getRedirectUrl(): Observable<string> {
|
||||||
const redirectUrl = this.storage.get(REDIRECT_COOKIE);
|
return this.store.pipe(
|
||||||
if (isNotEmpty(redirectUrl)) {
|
select(getRedirectUrl),
|
||||||
return observableOf(redirectUrl);
|
map((urlFromStore: string) => {
|
||||||
} else {
|
if (hasValue(urlFromStore)) {
|
||||||
return this.store.pipe(select(getRedirectUrl));
|
return urlFromStore;
|
||||||
}
|
} else {
|
||||||
|
return this.storage.get(REDIRECT_COOKIE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -488,6 +435,20 @@ export class AuthService {
|
|||||||
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the redirect url if the current one has not been set yet
|
||||||
|
* @param newRedirectUrl
|
||||||
|
*/
|
||||||
|
setRedirectUrlIfNotSet(newRedirectUrl: string) {
|
||||||
|
this.getRedirectUrl().pipe(
|
||||||
|
take(1))
|
||||||
|
.subscribe((currentRedirectUrl) => {
|
||||||
|
if (hasNoValue(currentRedirectUrl)) {
|
||||||
|
this.setRedirectUrl(newRedirectUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear redirect url
|
* Clear redirect url
|
||||||
*/
|
*/
|
||||||
|
@@ -1,21 +1,26 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivate,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { map, find, switchMap } from 'rxjs/operators';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { isAuthenticated } from './selectors';
|
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService, LOGIN_ROUTE } from './auth.service';
|
||||||
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes
|
* Prevent unauthorized activating and loading of routes
|
||||||
* @class AuthenticatedGuard
|
* @class AuthenticatedGuard
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthenticatedGuard implements CanActivate, CanLoad {
|
export class AuthenticatedGuard implements CanActivate {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
@@ -24,46 +29,37 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* True when user is authenticated
|
* True when user is authenticated
|
||||||
|
* UrlTree with redirect to login page when user isn't authenticated
|
||||||
* @method canActivate
|
* @method canActivate
|
||||||
*/
|
*/
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
const url = state.url;
|
const url = state.url;
|
||||||
return this.handleAuth(url);
|
return this.handleAuth(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when user is authenticated
|
* True when user is authenticated
|
||||||
|
* UrlTree with redirect to login page when user isn't authenticated
|
||||||
* @method canActivateChild
|
* @method canActivateChild
|
||||||
*/
|
*/
|
||||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
return this.canActivate(route, state);
|
return this.canActivate(route, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private handleAuth(url: string): Observable<boolean | UrlTree> {
|
||||||
* True when user is authenticated
|
|
||||||
* @method canLoad
|
|
||||||
*/
|
|
||||||
canLoad(route: Route): Observable<boolean> {
|
|
||||||
const url = `/${route.path}`;
|
|
||||||
|
|
||||||
return this.handleAuth(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAuth(url: string): Observable<boolean> {
|
|
||||||
// get observable
|
|
||||||
const observable = this.store.pipe(select(isAuthenticated));
|
|
||||||
|
|
||||||
// redirect to sign in page if user is not authenticated
|
// redirect to sign in page if user is not authenticated
|
||||||
observable.pipe(
|
return this.store.pipe(select(isAuthenticationLoading)).pipe(
|
||||||
// .filter(() => isEmpty(this.router.routerState.snapshot.url) || this.router.routerState.snapshot.url === url)
|
find((isLoading: boolean) => isLoading === false),
|
||||||
take(1))
|
switchMap(() => this.store.pipe(select(isAuthenticated))),
|
||||||
.subscribe((authenticated) => {
|
map((authenticated) => {
|
||||||
if (!authenticated) {
|
if (authenticated) {
|
||||||
|
return authenticated;
|
||||||
|
} else {
|
||||||
this.authService.setRedirectUrl(url);
|
this.authService.setRedirectUrl(url);
|
||||||
this.store.dispatch(new RedirectWhenAuthenticationIsRequiredAction('Login required'));
|
this.authService.removeToken();
|
||||||
|
return this.router.createUrlTree([LOGIN_ROUTE]);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
return observable;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,14 @@ const _getAuthenticationInfo = (state: AuthState) => state.info;
|
|||||||
*/
|
*/
|
||||||
const _isLoading = (state: AuthState) => state.loading;
|
const _isLoading = (state: AuthState) => state.loading;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if everything else should wait for authentication.
|
||||||
|
* @function _isBlocking
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isBlocking = (state: AuthState) => state.blocking;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a refresh token request is in progress.
|
* Returns true if a refresh token request is in progress.
|
||||||
* @function _isRefreshing
|
* @function _isRefreshing
|
||||||
@@ -170,6 +178,16 @@ export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticat
|
|||||||
*/
|
*/
|
||||||
export const isAuthenticationLoading = createSelector(getAuthState, _isLoading);
|
export const isAuthenticationLoading = createSelector(getAuthState, _isLoading);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the authentication should block everything else
|
||||||
|
*
|
||||||
|
* @function isAuthenticationBlocking
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isAuthenticationBlocking = createSelector(getAuthState, _isBlocking);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the refresh token request is loading.
|
* Returns true if the refresh token request is loading.
|
||||||
* @function isTokenRefreshing
|
* @function isTokenRefreshing
|
||||||
|
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
@@ -58,32 +58,4 @@ export class ServerAuthService extends AuthService {
|
|||||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect to the route navigated before the login
|
|
||||||
*/
|
|
||||||
public redirectAfterLoginSuccess(isStandalonePage: boolean) {
|
|
||||||
this.getRedirectUrl().pipe(
|
|
||||||
take(1))
|
|
||||||
.subscribe((redirectUrl) => {
|
|
||||||
if (isNotEmpty(redirectUrl)) {
|
|
||||||
// override the route reuse strategy
|
|
||||||
this.router.routeReuseStrategy.shouldReuseRoute = () => {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
this.router.navigated = false;
|
|
||||||
const url = decodeURIComponent(redirectUrl);
|
|
||||||
this.router.navigateByUrl(url);
|
|
||||||
} else {
|
|
||||||
// If redirectUrl is empty use history. For ssr the history array should contain the requested url.
|
|
||||||
this.routeService.getHistory().pipe(
|
|
||||||
filter((history) => history.length > 0),
|
|
||||||
take(1)
|
|
||||||
).subscribe((history) => {
|
|
||||||
this.navigateToRedirectUrl(history[history.length - 1] || '');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
12
src/app/core/cache/response.models.ts
vendored
12
src/app/core/cache/response.models.ts
vendored
@@ -5,7 +5,6 @@ import { PageInfo } from '../shared/page-info.model';
|
|||||||
import { ConfigObject } from '../config/models/config.model';
|
import { ConfigObject } from '../config/models/config.model';
|
||||||
import { FacetValue } from '../../shared/search/facet-value.model';
|
import { FacetValue } from '../../shared/search/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
import { SearchFilterConfig } from '../../shared/search/search-filter-config.model';
|
||||||
import { IntegrationModel } from '../integration/models/integration.model';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { SubmissionObject } from '../submission/models/submission-object.model';
|
import { SubmissionObject } from '../submission/models/submission-object.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
@@ -181,17 +180,6 @@ export class TokenResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IntegrationSuccessResponse extends RestResponse {
|
|
||||||
constructor(
|
|
||||||
public dataDefinition: PaginatedList<IntegrationModel>,
|
|
||||||
public statusCode: number,
|
|
||||||
public statusText: string,
|
|
||||||
public pageInfo?: PageInfo
|
|
||||||
) {
|
|
||||||
super(true, statusCode, statusText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PostPatchSuccessResponse extends RestResponse {
|
export class PostPatchSuccessResponse extends RestResponse {
|
||||||
constructor(
|
constructor(
|
||||||
public dataDefinition: any,
|
public dataDefinition: any,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
|
||||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
@@ -15,8 +16,8 @@ import { MenuService } from '../shared/menu/menu.service';
|
|||||||
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service';
|
||||||
import {
|
import {
|
||||||
MOCK_RESPONSE_MAP,
|
MOCK_RESPONSE_MAP,
|
||||||
ResponseMapMock,
|
mockResponseMap,
|
||||||
mockResponseMap
|
ResponseMapMock
|
||||||
} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock';
|
} from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock';
|
||||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||||
@@ -80,9 +81,6 @@ import { EPersonDataService } from './eperson/eperson-data.service';
|
|||||||
import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service';
|
import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service';
|
||||||
import { EPerson } from './eperson/models/eperson.model';
|
import { EPerson } from './eperson/models/eperson.model';
|
||||||
import { Group } from './eperson/models/group.model';
|
import { Group } from './eperson/models/group.model';
|
||||||
import { AuthorityService } from './integration/authority.service';
|
|
||||||
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
|
|
||||||
import { AuthorityValue } from './integration/models/authority.value';
|
|
||||||
import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder';
|
import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder';
|
||||||
import { MetadataField } from './metadata/metadata-field.model';
|
import { MetadataField } from './metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from './metadata/metadata-schema.model';
|
import { MetadataSchema } from './metadata/metadata-schema.model';
|
||||||
@@ -160,8 +158,19 @@ import { SubmissionCcLicenseDataService } from './submission/submission-cc-licen
|
|||||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||||
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
|
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
|
||||||
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
||||||
|
import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model';
|
||||||
|
import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
|
||||||
|
import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service';
|
||||||
|
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
|
||||||
|
import { VocabularyService } from './submission/vocabularies/vocabulary.service';
|
||||||
|
import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
|
||||||
import { ConfigurationDataService } from './data/configuration-data.service';
|
import { ConfigurationDataService } from './data/configuration-data.service';
|
||||||
import { ConfigurationProperty } from './shared/configuration-property.model';
|
import { ConfigurationProperty } from './shared/configuration-property.model';
|
||||||
|
import { ReloadGuard } from './reload/reload.guard';
|
||||||
|
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
|
import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard';
|
||||||
|
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
|
||||||
|
import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -195,7 +204,7 @@ const PROVIDERS = [
|
|||||||
SiteDataService,
|
SiteDataService,
|
||||||
DSOResponseParsingService,
|
DSOResponseParsingService,
|
||||||
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
|
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
|
||||||
{ provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient]},
|
{ provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] },
|
||||||
DynamicFormLayoutService,
|
DynamicFormLayoutService,
|
||||||
DynamicFormService,
|
DynamicFormService,
|
||||||
DynamicFormValidationService,
|
DynamicFormValidationService,
|
||||||
@@ -237,8 +246,6 @@ const PROVIDERS = [
|
|||||||
SubmissionResponseParsingService,
|
SubmissionResponseParsingService,
|
||||||
SubmissionJsonPatchOperationsService,
|
SubmissionJsonPatchOperationsService,
|
||||||
JsonPatchOperationsBuilder,
|
JsonPatchOperationsBuilder,
|
||||||
AuthorityService,
|
|
||||||
IntegrationResponseParsingService,
|
|
||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
@@ -286,9 +293,14 @@ const PROVIDERS = [
|
|||||||
FeatureDataService,
|
FeatureDataService,
|
||||||
AuthorizationDataService,
|
AuthorizationDataService,
|
||||||
SiteAdministratorGuard,
|
SiteAdministratorGuard,
|
||||||
|
SiteRegisterGuard,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
TokenResponseParsingService,
|
TokenResponseParsingService,
|
||||||
|
ReloadGuard,
|
||||||
|
EndUserAgreementCurrentUserGuard,
|
||||||
|
EndUserAgreementCookieGuard,
|
||||||
|
EndUserAgreementService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
@@ -303,7 +315,10 @@ const PROVIDERS = [
|
|||||||
},
|
},
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||||
|
VocabularyService,
|
||||||
|
VocabularyEntriesResponseParsingService,
|
||||||
|
VocabularyTreeviewService
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,7 +349,6 @@ export const models =
|
|||||||
SubmissionSectionModel,
|
SubmissionSectionModel,
|
||||||
SubmissionUploadsModel,
|
SubmissionUploadsModel,
|
||||||
AuthStatus,
|
AuthStatus,
|
||||||
AuthorityValue,
|
|
||||||
BrowseEntry,
|
BrowseEntry,
|
||||||
BrowseDefinition,
|
BrowseDefinition,
|
||||||
ClaimedTask,
|
ClaimedTask,
|
||||||
@@ -354,6 +368,9 @@ export const models =
|
|||||||
Feature,
|
Feature,
|
||||||
Authorization,
|
Authorization,
|
||||||
Registration,
|
Registration,
|
||||||
|
Vocabulary,
|
||||||
|
VocabularyEntry,
|
||||||
|
VocabularyEntryDetail,
|
||||||
ConfigurationProperty
|
ConfigurationProperty
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -1,40 +1,22 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ErrorResponse, GenericSuccessResponse, 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 { BrowseEntry } from '../shared/browse-entry.model';
|
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
import { EntriesResponseParsingService } from './entries-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { RestRequest } from './request.models';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
export class BrowseEntriesResponseParsingService extends EntriesResponseParsingService<BrowseEntry> {
|
||||||
|
|
||||||
protected toCache = false;
|
protected toCache = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
) { super();
|
) {
|
||||||
|
super(objectCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
getSerializerModel(): GenericConstructor<BrowseEntry> {
|
||||||
if (isNotEmpty(data.payload)) {
|
return BrowseEntry;
|
||||||
let browseEntries = [];
|
|
||||||
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
|
||||||
const serializer = new DSpaceSerializer(BrowseEntry);
|
|
||||||
browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
|
||||||
}
|
|
||||||
return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
|
||||||
} else {
|
|
||||||
return new ErrorResponse(
|
|
||||||
Object.assign(
|
|
||||||
new Error('Unexpected response from browse endpoint'),
|
|
||||||
{ statusCode: data.statusCode, statusText: data.statusText }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,11 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { FindListOptions, PatchRequest } from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
|
@@ -6,18 +6,18 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
|
|||||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { ContentSourceRequest, GetRequest, RequestError, UpdateContentSourceRequest } from './request.models';
|
import { ContentSourceRequest, GetRequest, UpdateContentSourceRequest } from './request.models';
|
||||||
import { ContentSource } from '../shared/content-source.model';
|
import { ContentSource } from '../shared/content-source.model';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
import { ErrorResponse } from '../cache/response.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||||
import { hot, getTestScheduler, cold } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
|
@@ -24,7 +24,7 @@ import { RequestService } from './request.service';
|
|||||||
@dataService(COMMUNITY)
|
@dataService(COMMUNITY)
|
||||||
export class CommunityDataService extends ComColDataService<Community> {
|
export class CommunityDataService extends ComColDataService<Community> {
|
||||||
protected linkPath = 'communities';
|
protected linkPath = 'communities';
|
||||||
protected topLinkPath = 'communities/search/top';
|
protected topLinkPath = 'search/top';
|
||||||
protected cds = this;
|
protected cds = this;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@@ -18,6 +18,7 @@ import { FindListOptions, PatchRequest } from './request.models';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
const endpoint = 'https://rest.api/core';
|
const endpoint = 'https://rest.api/core';
|
||||||
|
|
||||||
@@ -150,7 +151,8 @@ describe('DataService', () => {
|
|||||||
currentPage: 6,
|
currentPage: 6,
|
||||||
elementsPerPage: 10,
|
elementsPerPage: 10,
|
||||||
sort: sortOptions,
|
sort: sortOptions,
|
||||||
startsWith: 'ab'
|
startsWith: 'ab',
|
||||||
|
|
||||||
};
|
};
|
||||||
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
|
||||||
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
|
||||||
@@ -160,6 +162,26 @@ describe('DataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should include all searchParams in href if any provided in options', () => {
|
||||||
|
options = { searchParams: [
|
||||||
|
new RequestParam('param1', 'test'),
|
||||||
|
new RequestParam('param2', 'test2'),
|
||||||
|
] };
|
||||||
|
const expected = `${endpoint}?param1=test¶m2=test2`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref(options).subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include linkPath in href if any provided', () => {
|
||||||
|
const expected = `${endpoint}/test/entries`;
|
||||||
|
|
||||||
|
(service as any).getFindAllHref({}, 'test/entries').subscribe((value) => {
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should include single linksToFollow as embed', () => {
|
it('should include single linksToFollow as embed', () => {
|
||||||
const expected = `${endpoint}?embed=bundles`;
|
const expected = `${endpoint}?embed=bundles`;
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
@@ -71,13 +71,17 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* Return an observable that emits created HREF
|
* Return an observable that emits created HREF
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
protected getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
||||||
let result$: Observable<string>;
|
let endpoint$: Observable<string>;
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
result$ = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged());
|
endpoint$ = this.getBrowseEndpoint(options).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href),
|
||||||
|
distinctUntilChanged()
|
||||||
|
);
|
||||||
|
|
||||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,18 +93,12 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* Return an observable that emits created HREF
|
* Return an observable that emits created HREF
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
protected getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
||||||
let result$: Observable<string>;
|
let result$: Observable<string>;
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
result$ = this.getSearchEndpoint(searchMethod);
|
result$ = this.getSearchEndpoint(searchMethod);
|
||||||
|
|
||||||
if (hasValue(options.searchParams)) {
|
|
||||||
options.searchParams.forEach((param: RequestParam) => {
|
|
||||||
args.push(`${param.fieldName}=${param.fieldValue}`);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +112,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* Return an observable that emits created HREF
|
* Return an observable that emits created HREF
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
|
||||||
let args = [...extraArgs];
|
let args = [...extraArgs];
|
||||||
|
|
||||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||||
@@ -130,6 +128,11 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
if (hasValue(options.startsWith)) {
|
if (hasValue(options.startsWith)) {
|
||||||
args = [...args, `startsWith=${options.startsWith}`];
|
args = [...args, `startsWith=${options.startsWith}`];
|
||||||
}
|
}
|
||||||
|
if (hasValue(options.searchParams)) {
|
||||||
|
options.searchParams.forEach((param: RequestParam) => {
|
||||||
|
args = [...args, `${param.fieldName}=${param.fieldValue}`];
|
||||||
|
})
|
||||||
|
}
|
||||||
args = this.addEmbedParams(args, ...linksToFollow);
|
args = this.addEmbedParams(args, ...linksToFollow);
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
return new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
@@ -373,11 +376,20 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
hasValueOperator(),
|
||||||
find((request: RequestEntry) => request.completed),
|
find((request: RequestEntry) => request.completed),
|
||||||
map((request: RequestEntry) => request.response)
|
map((request: RequestEntry) => request.response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createPatchFromCache(object: T): Observable<Operation[]> {
|
||||||
|
const oldVersion$ = this.findByHref(object._links.self.href);
|
||||||
|
return oldVersion$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((oldVersion: T) => this.comparator.diff(oldVersion, object)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a PUT request for the specified object
|
* Send a PUT request for the specified object
|
||||||
*
|
*
|
||||||
@@ -406,18 +418,16 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* @param {DSpaceObject} object The given object
|
* @param {DSpaceObject} object The given object
|
||||||
*/
|
*/
|
||||||
update(object: T): Observable<RemoteData<T>> {
|
update(object: T): Observable<RemoteData<T>> {
|
||||||
const oldVersion$ = this.findByHref(object._links.self.href);
|
return this.createPatchFromCache(object)
|
||||||
return oldVersion$.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
mergeMap((operations: Operation[]) => {
|
||||||
getRemoteDataPayload(),
|
if (isNotEmpty(operations)) {
|
||||||
mergeMap((oldVersion: T) => {
|
this.objectCache.addPatch(object._links.self.href, operations);
|
||||||
const operations = this.comparator.diff(oldVersion, object);
|
}
|
||||||
if (isNotEmpty(operations)) {
|
return this.findByHref(object._links.self.href);
|
||||||
this.objectCache.addPatch(object._links.self.href, operations);
|
|
||||||
}
|
}
|
||||||
return this.findByHref(object._links.self.href);
|
)
|
||||||
}
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
54
src/app/core/data/entries-response-parsing.service.ts
Normal file
54
src/app/core/data/entries-response-parsing.service.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ErrorResponse, GenericSuccessResponse, 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 { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract class to extend, responsible for parsing data for an entries response
|
||||||
|
*/
|
||||||
|
export abstract class EntriesResponseParsingService<T extends CacheableObject> extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to implement that must return the dspace serializer Constructor to use during parse
|
||||||
|
*/
|
||||||
|
abstract getSerializerModel(): GenericConstructor<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse response
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload)) {
|
||||||
|
let entries = [];
|
||||||
|
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
|
const serializer = new DSpaceSerializer(this.getSerializerModel());
|
||||||
|
entries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
}
|
||||||
|
return new GenericSuccessResponse(entries, data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from browse endpoint'),
|
||||||
|
{ statusCode: data.statusCode, statusText: data.statusText }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -63,33 +63,33 @@ describe('AuthorizationDataService', () => {
|
|||||||
return Object.assign(new FindListOptions(), { searchParams });
|
return Object.assign(new FindListOptions(), { searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('when no arguments are provided and a user is authenticated', () => {
|
describe('when no arguments are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject().subscribe();
|
service.searchByObject().subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url and authenticated user\'s uuid', () => {
|
it('should call searchBy with the site\'s url', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when no arguments except for a feature are provided and a user is authenticated', () => {
|
describe('when no arguments except for a feature are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject(FeatureID.LoginOnBehalfOf).subscribe();
|
service.searchByObject(FeatureID.LoginOnBehalfOf).subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url, authenticated user\'s uuid and the feature', () => {
|
it('should call searchBy with the site\'s url and the feature', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when a feature and object url are provided, but no user uuid and a user is authenticated', () => {
|
describe('when a feature and object url are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl).subscribe();
|
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl).subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the object\'s url, authenticated user\'s uuid and the feature', () => {
|
it('should call searchBy with the object\'s url and the feature', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,17 +102,6 @@ describe('AuthorizationDataService', () => {
|
|||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when no arguments are provided and no user is authenticated', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(authService, 'isAuthenticated').and.returnValue(observableOf(false));
|
|
||||||
service.searchByObject().subscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url', () => {
|
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isAuthorized', () => {
|
describe('isAuthorized', () => {
|
||||||
|
@@ -25,7 +25,6 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
|||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||||
import {
|
import {
|
||||||
addAuthenticatedUserUuidIfEmpty,
|
|
||||||
addSiteObjectUrlIfEmpty,
|
addSiteObjectUrlIfEmpty,
|
||||||
oneAuthorizationMatchesFeature
|
oneAuthorizationMatchesFeature
|
||||||
} from './authorization-utils';
|
} from './authorization-utils';
|
||||||
@@ -90,7 +89,6 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
||||||
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
||||||
addSiteObjectUrlIfEmpty(this.siteService),
|
addSiteObjectUrlIfEmpty(this.siteService),
|
||||||
addAuthenticatedUserUuidIfEmpty(this.authService),
|
|
||||||
switchMap((params: AuthorizationSearchParams) => {
|
switchMap((params: AuthorizationSearchParams) => {
|
||||||
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), ...linksToFollow);
|
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), ...linksToFollow);
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../remote-data';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of abstract class DsoPageAdministratorGuard
|
||||||
|
*/
|
||||||
|
class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
||||||
|
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected featureID: FeatureID) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(this.featureID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DsoPageAdministratorGuard', () => {
|
||||||
|
let guard: DsoPageFeatureGuard<any>;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let resolver: Resolve<RemoteData<any>>;
|
||||||
|
let object: DSpaceObject;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
object = {
|
||||||
|
self: 'test-selflink'
|
||||||
|
} as DSpaceObject;
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {}
|
||||||
|
});
|
||||||
|
resolver = jasmine.createSpyObj('resolver', {
|
||||||
|
resolve: createSuccessfulRemoteDataObject$(object)
|
||||||
|
});
|
||||||
|
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getObjectUrl', () => {
|
||||||
|
it('should return the resolved object\'s selflink', (done) => {
|
||||||
|
guard.getObjectUrl(undefined, undefined).subscribe((selflink) => {
|
||||||
|
expect(selflink).toEqual(object.self);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../remote-data';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||||
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
|
*/
|
||||||
|
export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected resolver: Resolve<RemoteData<T>>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authorization rights for the object resolved using the provided resolver
|
||||||
|
*/
|
||||||
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return (this.resolver.resolve(route, state) as Observable<RemoteData<T>>).pipe(
|
||||||
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
map((dso) => dso.self)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,8 @@ import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
|||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test implementation of abstract class FeatureAuthorizationGuard
|
* Test implementation of abstract class FeatureAuthorizationGuard
|
||||||
@@ -17,16 +18,16 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
|||||||
super(authorizationService, router);
|
super(authorizationService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatureID(): FeatureID {
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
return this.featureId;
|
return observableOf(this.featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectUrl(): string {
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return this.objectUrl;
|
return observableOf(this.objectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEPersonUuid(): string {
|
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return this.ePersonUuid;
|
return observableOf(this.ePersonUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,8 @@ import { AuthorizationDataService } from '../authorization-data.service';
|
|||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators';
|
import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators';
|
||||||
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
||||||
@@ -24,29 +26,32 @@ export abstract class FeatureAuthorizationGuard implements CanActivate {
|
|||||||
* True when user has authorization rights for the feature and object provided
|
* True when user has authorization rights for the feature and object provided
|
||||||
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
||||||
*/
|
*/
|
||||||
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
return this.authorizationService.isAuthorized(this.getFeatureID(), this.getObjectUrl(), this.getEPersonUuid()).pipe(returnUnauthorizedUrlTreeOnFalse(this.router));
|
return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||||
|
switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)),
|
||||||
|
returnUnauthorizedUrlTreeOnFalse(this.router)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of feature to check authorization for
|
* The type of feature to check authorization for
|
||||||
* Override this method to define a feature
|
* Override this method to define a feature
|
||||||
*/
|
*/
|
||||||
abstract getFeatureID(): FeatureID;
|
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL of the object to check if the user has authorized rights for
|
* The URL of the object to check if the user has authorized rights for
|
||||||
* Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used
|
* Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used
|
||||||
*/
|
*/
|
||||||
getObjectUrl(): string {
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return undefined;
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the user to check authorization rights for
|
* The UUID of the user to check authorization rights for
|
||||||
* Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used.
|
* Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used.
|
||||||
*/
|
*/
|
||||||
getEPersonUuid(): string {
|
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return undefined;
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { Router } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
||||||
@@ -19,7 +21,7 @@ export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
|
|||||||
/**
|
/**
|
||||||
* Check administrator authorization rights
|
* Check administrator authorization rights
|
||||||
*/
|
*/
|
||||||
getFeatureID(): FeatureID {
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
return FeatureID.AdministratorOf;
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration
|
||||||
|
* rights to the {@link Site}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SiteRegisterGuard extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check registration authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.EPersonRegistration);
|
||||||
|
}
|
||||||
|
}
|
@@ -3,5 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
export enum FeatureID {
|
export enum FeatureID {
|
||||||
LoginOnBehalfOf = 'loginOnBehalfOf',
|
LoginOnBehalfOf = 'loginOnBehalfOf',
|
||||||
AdministratorOf = 'administratorOf'
|
AdministratorOf = 'administratorOf',
|
||||||
|
WithdrawItem = 'withdrawItem',
|
||||||
|
ReinstateItem = 'reinstateItem',
|
||||||
|
EPersonRegistration = 'epersonRegistration',
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -27,6 +30,7 @@ import { RequestParam } from '../cache/models/request-param.model';
|
|||||||
export class MetadataFieldDataService extends DataService<MetadataField> {
|
export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||||
protected linkPath = 'metadatafields';
|
protected linkPath = 'metadatafields';
|
||||||
protected searchBySchemaLinkPath = 'bySchema';
|
protected searchBySchemaLinkPath = 'bySchema';
|
||||||
|
protected searchByFieldNameLinkPath = 'byFieldName';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -53,6 +57,43 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
|
|||||||
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find metadata fields with either the partial metadata field name (e.g. "dc.ti") as query or an exact match to
|
||||||
|
* at least the schema, element or qualifier
|
||||||
|
* @param schema optional; an exact match of the prefix of the metadata schema (e.g. "dc", "dcterms", "eperson")
|
||||||
|
* @param element optional; an exact match of the field's element (e.g. "contributor", "title")
|
||||||
|
* @param qualifier optional; an exact match of the field's qualifier (e.g. "author", "alternative")
|
||||||
|
* @param query optional (if any of schema, element or qualifier used) - part of the fully qualified field,
|
||||||
|
* should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”)
|
||||||
|
* @param exactName optional; the exact fully qualified field, should use the syntax schema.element.qualifier or
|
||||||
|
* schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value
|
||||||
|
* if there's an exact match
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
searchByFieldNameParams(schema: string, element: string, qualifier: string, query: string, exactName: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
|
const optionParams = Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [
|
||||||
|
new RequestParam('schema', hasValue(schema) ? schema : ''),
|
||||||
|
new RequestParam('element', hasValue(element) ? element : ''),
|
||||||
|
new RequestParam('qualifier', hasValue(qualifier) ? qualifier : ''),
|
||||||
|
new RequestParam('query', hasValue(query) ? query : ''),
|
||||||
|
new RequestParam('exactName', hasValue(exactName) ? exactName : '')
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return this.searchBy(this.searchByFieldNameLinkPath, optionParams, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a specific metadata field by name.
|
||||||
|
* @param exactFieldName The exact fully qualified field, should use the syntax schema.element.qualifier or
|
||||||
|
* schema.element if no qualifier exists (e.g. "dc.title", "dc.contributor.author"). It will only return one value
|
||||||
|
* if there's an exact match, empty list if there is no exact match.
|
||||||
|
*/
|
||||||
|
findByExactFieldName(exactFieldName: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
|
return this.searchByFieldNameParams(null, null, null, null, exactFieldName, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all metadata field requests
|
* Clear all metadata field requests
|
||||||
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||||
|
@@ -9,7 +9,6 @@ import { ConfigResponseParsingService } from '../config/config-response-parsing.
|
|||||||
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
|
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
|
import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service';
|
||||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service';
|
||||||
@@ -20,12 +19,13 @@ import { ContentSourceResponseParsingService } from './content-source-response-p
|
|||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
||||||
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
import { VocabularyEntriesResponseParsingService } from '../submission/vocabularies/vocabulary-entries-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
// uuid and handle requests have separate endpoints
|
// uuid and handle requests have separate endpoints
|
||||||
export enum IdentifierType {
|
export enum IdentifierType {
|
||||||
UUID ='uuid',
|
UUID = 'uuid',
|
||||||
HANDLE = 'handle'
|
HANDLE = 'handle'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export class GetRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.GET, body, options)
|
super(uuid, href, RestRequestMethod.GET, body, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export class PostRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.POST, body)
|
super(uuid, href, RestRequestMethod.POST, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export class PutRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.PUT, body)
|
super(uuid, href, RestRequestMethod.PUT, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export class DeleteRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.DELETE, body)
|
super(uuid, href, RestRequestMethod.DELETE, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ export class OptionsRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.OPTIONS, body)
|
super(uuid, href, RestRequestMethod.OPTIONS, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ export class HeadRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.HEAD, body)
|
super(uuid, href, RestRequestMethod.HEAD, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export class PatchRequest extends RestRequest {
|
|||||||
public href: string,
|
public href: string,
|
||||||
public body?: any,
|
public body?: any,
|
||||||
public options?: HttpOptions
|
public options?: HttpOptions
|
||||||
) {
|
) {
|
||||||
super(uuid, href, RestRequestMethod.PATCH, body)
|
super(uuid, href, RestRequestMethod.PATCH, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,16 +276,6 @@ export class TokenPostRequest extends PostRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IntegrationRequest extends GetRequest {
|
|
||||||
constructor(uuid: string, href: string) {
|
|
||||||
super(uuid, href);
|
|
||||||
}
|
|
||||||
|
|
||||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
|
||||||
return IntegrationResponseParsingService;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a submission HTTP GET request object
|
* Class representing a submission HTTP GET request object
|
||||||
*/
|
*/
|
||||||
@@ -425,6 +415,15 @@ export class MyDSpaceRequest extends GetRequest {
|
|||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to get vocabulary entries
|
||||||
|
*/
|
||||||
|
export class VocabularyEntriesRequest extends FindListRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return VocabularyEntriesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
|
||||||
|
* That condition is defined by abstract method hasAccepted
|
||||||
|
*/
|
||||||
|
export abstract class AbstractEndUserAgreementGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(protected router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the user agreement has been accepted
|
||||||
|
* The user will be redirected to the End User Agreement page if they haven't accepted it before
|
||||||
|
* A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route
|
||||||
|
* when they're finished accepting the agreement
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
|
return this.hasAccepted().pipe(
|
||||||
|
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This abstract method determines how the User Agreement has to be accepted before the user is allowed to visit
|
||||||
|
* the desired route
|
||||||
|
*/
|
||||||
|
abstract hasAccepted(): Observable<boolean>;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
import { Router, UrlTree } from '@angular/router';
|
||||||
|
import { EndUserAgreementCookieGuard } from './end-user-agreement-cookie.guard';
|
||||||
|
|
||||||
|
describe('EndUserAgreementCookieGuard', () => {
|
||||||
|
let guard: EndUserAgreementCookieGuard;
|
||||||
|
|
||||||
|
let endUserAgreementService: EndUserAgreementService;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
|
||||||
|
isCookieAccepted: true
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: {},
|
||||||
|
parseUrl: new UrlTree(),
|
||||||
|
createUrlTree: new UrlTree()
|
||||||
|
});
|
||||||
|
|
||||||
|
guard = new EndUserAgreementCookieGuard(endUserAgreementService, router);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
describe('when the cookie has been accepted', () => {
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(undefined, { url: Object.assign({ url: 'redirect' }) } as any).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie hasn\'t been accepted', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(endUserAgreementService.isCookieAccepted as jasmine.Spy).and.returnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a UrlTree', (done) => {
|
||||||
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
|
expect(result).toEqual(jasmine.any(UrlTree));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard redirecting users to the end agreement page when the user agreement cookie hasn't been accepted
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EndUserAgreementCookieGuard extends AbstractEndUserAgreementGuard {
|
||||||
|
|
||||||
|
constructor(protected endUserAgreementService: EndUserAgreementService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the user agreement cookie has been accepted
|
||||||
|
*/
|
||||||
|
hasAccepted(): Observable<boolean> {
|
||||||
|
return observableOf(this.endUserAgreementService.isCookieAccepted());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard';
|
||||||
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
import { Router, UrlTree } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
|
describe('EndUserAgreementGuard', () => {
|
||||||
|
let guard: EndUserAgreementCurrentUserGuard;
|
||||||
|
|
||||||
|
let endUserAgreementService: EndUserAgreementService;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', {
|
||||||
|
hasCurrentUserAcceptedAgreement: observableOf(true)
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: {},
|
||||||
|
parseUrl: new UrlTree(),
|
||||||
|
createUrlTree: new UrlTree()
|
||||||
|
});
|
||||||
|
|
||||||
|
guard = new EndUserAgreementCurrentUserGuard(endUserAgreementService, router);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
describe('when the user has accepted the agreement', () => {
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user hasn\'t accepted the agreement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(endUserAgreementService.hasCurrentUserAcceptedAgreement as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a UrlTree', (done) => {
|
||||||
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
|
expect(result).toEqual(jasmine.any(UrlTree));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
||||||
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGuard {
|
||||||
|
|
||||||
|
constructor(protected endUserAgreementService: EndUserAgreementService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
|
||||||
|
*/
|
||||||
|
hasAccepted(): Observable<boolean> {
|
||||||
|
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
END_USER_AGREEMENT_COOKIE,
|
||||||
|
END_USER_AGREEMENT_METADATA_FIELD,
|
||||||
|
EndUserAgreementService
|
||||||
|
} from './end-user-agreement.service';
|
||||||
|
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
describe('EndUserAgreementService', () => {
|
||||||
|
let service: EndUserAgreementService;
|
||||||
|
|
||||||
|
let userWithMetadata: EPerson;
|
||||||
|
let userWithoutMetadata: EPerson;
|
||||||
|
|
||||||
|
let cookie;
|
||||||
|
let authService;
|
||||||
|
let ePersonService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
userWithMetadata = Object.assign(new EPerson(), {
|
||||||
|
metadata: {
|
||||||
|
[END_USER_AGREEMENT_METADATA_FIELD]: [
|
||||||
|
{
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
userWithoutMetadata = Object.assign(new EPerson());
|
||||||
|
|
||||||
|
cookie = new CookieServiceMock();
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: observableOf(userWithMetadata)
|
||||||
|
});
|
||||||
|
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||||
|
update: createSuccessfulRemoteDataObject$(userWithMetadata),
|
||||||
|
patch: observableOf(new RestResponse(true, 200, 'OK'))
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new EndUserAgreementService(cookie, authService, ePersonService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie is set to true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cookie.set(END_USER_AGREEMENT_COOKIE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasCurrentUserOrCookieAcceptedAgreement should return true', (done) => {
|
||||||
|
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isCookieAccepted should return true', () => {
|
||||||
|
expect(service.isCookieAccepted()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeCookieAccepted should remove the cookie', () => {
|
||||||
|
service.removeCookieAccepted();
|
||||||
|
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the cookie isn\'t set', () => {
|
||||||
|
describe('and the user is authenticated', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and the user contains agreement metadata', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf(userWithMetadata));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasCurrentUserOrCookieAcceptedAgreement should return true', (done) => {
|
||||||
|
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and the user doesn\'t contain agreement metadata', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf(userWithoutMetadata));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasCurrentUserOrCookieAcceptedAgreement should return false', (done) => {
|
||||||
|
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setUserAcceptedAgreement should update the user with new metadata', (done) => {
|
||||||
|
service.setUserAcceptedAgreement(true).subscribe(() => {
|
||||||
|
expect(ePersonService.patch).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and the user is not authenticated', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasCurrentUserOrCookieAcceptedAgreement should return false', (done) => {
|
||||||
|
service.hasCurrentUserOrCookieAcceptedAgreement(false).subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setUserAcceptedAgreement should set the cookie to true', (done) => {
|
||||||
|
service.setUserAcceptedAgreement(true).subscribe(() => {
|
||||||
|
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isCookieAccepted should return false', () => {
|
||||||
|
expect(service.isCookieAccepted()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setCookieAccepted should set the cookie', () => {
|
||||||
|
service.setCookieAccepted(true);
|
||||||
|
expect(cookie.get(END_USER_AGREEMENT_COOKIE)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
111
src/app/core/end-user-agreement/end-user-agreement.service.ts
Normal file
111
src/app/core/end-user-agreement/end-user-agreement.service.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
|
|
||||||
|
export const END_USER_AGREEMENT_COOKIE = 'hasAgreedEndUser';
|
||||||
|
export const END_USER_AGREEMENT_METADATA_FIELD = 'dspace.agreements.end-user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for checking and managing the status of the current end user agreement
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EndUserAgreementService {
|
||||||
|
|
||||||
|
constructor(protected cookie: CookieService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected ePersonService: EPersonDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not either the cookie was accepted or the current user has accepted the End User Agreement
|
||||||
|
* @param acceptedWhenAnonymous Whether or not the user agreement should be considered accepted if the user is
|
||||||
|
* currently not authenticated (anonymous)
|
||||||
|
*/
|
||||||
|
hasCurrentUserOrCookieAcceptedAgreement(acceptedWhenAnonymous: boolean): Observable<boolean> {
|
||||||
|
if (this.isCookieAccepted()) {
|
||||||
|
return observableOf(true);
|
||||||
|
} else {
|
||||||
|
return this.hasCurrentUserAcceptedAgreement(acceptedWhenAnonymous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current user has accepted the End User Agreement
|
||||||
|
* @param acceptedWhenAnonymous Whether or not the user agreement should be considered accepted if the user is
|
||||||
|
* currently not authenticated (anonymous)
|
||||||
|
*/
|
||||||
|
hasCurrentUserAcceptedAgreement(acceptedWhenAnonymous: boolean): Observable<boolean> {
|
||||||
|
return this.authService.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => {
|
||||||
|
if (authenticated) {
|
||||||
|
return this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
map((user) => hasValue(user) && user.hasMetadata(END_USER_AGREEMENT_METADATA_FIELD) && user.firstMetadata(END_USER_AGREEMENT_METADATA_FIELD).value === 'true')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(acceptedWhenAnonymous);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current user's accepted agreement status
|
||||||
|
* When a user is authenticated, set his/her metadata to the provided value
|
||||||
|
* When no user is authenticated, set the cookie to the provided value
|
||||||
|
* @param accepted
|
||||||
|
*/
|
||||||
|
setUserAcceptedAgreement(accepted: boolean): Observable<boolean> {
|
||||||
|
return this.authService.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => {
|
||||||
|
if (authenticated) {
|
||||||
|
return this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((user) => {
|
||||||
|
const newValue = { value: String(accepted) };
|
||||||
|
let operation;
|
||||||
|
if (user.hasMetadata(END_USER_AGREEMENT_METADATA_FIELD)) {
|
||||||
|
operation = { op: 'replace', path: `/metadata/${END_USER_AGREEMENT_METADATA_FIELD}/0`, value: newValue };
|
||||||
|
} else {
|
||||||
|
operation = { op: 'add', path: `/metadata/${END_USER_AGREEMENT_METADATA_FIELD}`, value: [ newValue ] };
|
||||||
|
}
|
||||||
|
return this.ePersonService.patch(user, [operation]);
|
||||||
|
}),
|
||||||
|
map((response) => response.isSuccessful)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.setCookieAccepted(accepted);
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the End User Agreement accepted in the cookie?
|
||||||
|
*/
|
||||||
|
isCookieAccepted(): boolean {
|
||||||
|
return this.cookie.get(END_USER_AGREEMENT_COOKIE) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cookie's End User Agreement accepted state
|
||||||
|
* @param accepted
|
||||||
|
*/
|
||||||
|
setCookieAccepted(accepted: boolean) {
|
||||||
|
this.cookie.set(END_USER_AGREEMENT_COOKIE, accepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the End User Agreement cookie
|
||||||
|
*/
|
||||||
|
removeCookieAccepted() {
|
||||||
|
this.cookie.remove(END_USER_AGREEMENT_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, map, take, tap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
GroupRegistryCancelGroupAction,
|
GroupRegistryCancelGroupAction,
|
||||||
GroupRegistryEditGroupAction
|
GroupRegistryEditGroupAction
|
||||||
@@ -21,18 +21,12 @@ import { DataService } from '../data/data.service';
|
|||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import {
|
import { CreateRequest, DeleteRequest, FindListOptions, FindListRequest, PostRequest } from '../data/request.models';
|
||||||
CreateRequest,
|
|
||||||
DeleteRequest,
|
|
||||||
FindListOptions,
|
|
||||||
FindListRequest,
|
|
||||||
PostRequest
|
|
||||||
} from '../data/request.models';
|
|
||||||
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { configureRequest, getResponseFromEntry} from '../shared/operators';
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
import { EPerson } from './models/eperson.model';
|
import { EPerson } from './models/eperson.model';
|
||||||
import { Group } from './models/group.model';
|
import { Group } from './models/group.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { IntegrationService } from './integration.service';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthorityService extends IntegrationService {
|
|
||||||
protected linkPath = 'authorities';
|
|
||||||
protected entriesEndpoint = 'entries';
|
|
||||||
protected entryValueEndpoint = 'entryValues';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected requestService: RequestService,
|
|
||||||
protected rdbService: RemoteDataBuildService,
|
|
||||||
protected halService: HALEndpointService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { IntegrationModel } from './models/integration.model';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class to represent the data retrieved by an Integration service
|
|
||||||
*/
|
|
||||||
export class IntegrationData {
|
|
||||||
constructor(
|
|
||||||
public pageInfo: PageInfo,
|
|
||||||
public payload: IntegrationModel[]
|
|
||||||
) { }
|
|
||||||
}
|
|
@@ -1,221 +0,0 @@
|
|||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
|
||||||
import { IntegrationRequest } from '../data/request.models';
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { IntegrationResponseParsingService } from './integration-response-parsing.service';
|
|
||||||
import { AuthorityValue } from './models/authority.value';
|
|
||||||
|
|
||||||
describe('IntegrationResponseParsingService', () => {
|
|
||||||
let service: IntegrationResponseParsingService;
|
|
||||||
|
|
||||||
const store = {} as Store<CoreState>;
|
|
||||||
const objectCacheService = new ObjectCacheService(store, undefined);
|
|
||||||
const name = 'type';
|
|
||||||
const metadata = 'dc.type';
|
|
||||||
const query = '';
|
|
||||||
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
|
||||||
const integrationEndpoint = 'https://rest.api/integration/authorities';
|
|
||||||
const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
|
||||||
let validRequest;
|
|
||||||
|
|
||||||
let validResponse;
|
|
||||||
|
|
||||||
let invalidResponse1;
|
|
||||||
let invalidResponse2;
|
|
||||||
let pageInfo;
|
|
||||||
let definitions;
|
|
||||||
|
|
||||||
function initVars() {
|
|
||||||
pageInfo = Object.assign(new PageInfo(), {
|
|
||||||
elementsPerPage: 5,
|
|
||||||
totalElements: 5,
|
|
||||||
totalPages: 1,
|
|
||||||
currentPage: 1,
|
|
||||||
_links: {
|
|
||||||
self: { href: 'https://rest.api/integration/authorities/type/entries' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
definitions = new PaginatedList(pageInfo, [
|
|
||||||
Object.assign(new AuthorityValue(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'One',
|
|
||||||
id: 'One',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'One'
|
|
||||||
}),
|
|
||||||
Object.assign(new AuthorityValue(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Two',
|
|
||||||
id: 'Two',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Two'
|
|
||||||
}),
|
|
||||||
Object.assign(new AuthorityValue(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Three',
|
|
||||||
id: 'Three',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Three'
|
|
||||||
}),
|
|
||||||
Object.assign(new AuthorityValue(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Four',
|
|
||||||
id: 'Four',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Four'
|
|
||||||
}),
|
|
||||||
Object.assign(new AuthorityValue(), {
|
|
||||||
type: 'authority',
|
|
||||||
display: 'Five',
|
|
||||||
id: 'Five',
|
|
||||||
otherInformation: undefined,
|
|
||||||
value: 'Five'
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
|
|
||||||
|
|
||||||
validResponse = {
|
|
||||||
payload: {
|
|
||||||
page: {
|
|
||||||
number: 0,
|
|
||||||
size: 5,
|
|
||||||
totalElements: 5,
|
|
||||||
totalPages: 1
|
|
||||||
},
|
|
||||||
_embedded: {
|
|
||||||
authorityEntries: [
|
|
||||||
{
|
|
||||||
display: 'One',
|
|
||||||
id: 'One',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'One'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Two',
|
|
||||||
id: 'Two',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Two'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Three',
|
|
||||||
id: 'Three',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Three'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Four',
|
|
||||||
id: 'Four',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Four'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Five',
|
|
||||||
id: 'Five',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Five'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
},
|
|
||||||
_links: {
|
|
||||||
self: { href: 'https://rest.api/integration/authorities/type/entries' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
statusCode: 200,
|
|
||||||
statusText: 'OK'
|
|
||||||
};
|
|
||||||
|
|
||||||
invalidResponse1 = {
|
|
||||||
payload: {},
|
|
||||||
statusCode: 400,
|
|
||||||
statusText: 'Bad Request'
|
|
||||||
};
|
|
||||||
|
|
||||||
invalidResponse2 = {
|
|
||||||
payload: {
|
|
||||||
page: {
|
|
||||||
number: 0,
|
|
||||||
size: 5,
|
|
||||||
totalElements: 5,
|
|
||||||
totalPages: 1
|
|
||||||
},
|
|
||||||
_embedded: {
|
|
||||||
authorityEntries: [
|
|
||||||
{
|
|
||||||
display: 'One',
|
|
||||||
id: 'One',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'One'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Two',
|
|
||||||
id: 'Two',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Two'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Three',
|
|
||||||
id: 'Three',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Three'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Four',
|
|
||||||
id: 'Four',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Four'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display: 'Five',
|
|
||||||
id: 'Five',
|
|
||||||
otherInformation: {},
|
|
||||||
type: 'authority',
|
|
||||||
value: 'Five'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
},
|
|
||||||
_links: {}
|
|
||||||
},
|
|
||||||
statusCode: 500,
|
|
||||||
statusText: 'Internal Server Error'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
beforeEach(() => {
|
|
||||||
initVars();
|
|
||||||
service = new IntegrationResponseParsingService(objectCacheService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parse', () => {
|
|
||||||
it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => {
|
|
||||||
const response = service.parse(validRequest, validResponse);
|
|
||||||
expect(response.constructor).toBe(IntegrationSuccessResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an ErrorResponse if data contains an invalid config endpoint response', () => {
|
|
||||||
const response1 = service.parse(validRequest, invalidResponse1);
|
|
||||||
const response2 = service.parse(validRequest, invalidResponse2);
|
|
||||||
expect(response1.constructor).toBe(ErrorResponse);
|
|
||||||
expect(response2.constructor).toBe(ErrorResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a IntegrationSuccessResponse with data definition', () => {
|
|
||||||
const response = service.parse(validRequest, validResponse);
|
|
||||||
expect((response as any).dataDefinition).toEqual(definitions);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,50 +0,0 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
|
||||||
import { RestRequest } from '../data/request.models';
|
|
||||||
import { ResponseParsingService } from '../data/parsing.service';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
|
||||||
import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response.models';
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
|
||||||
|
|
||||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { IntegrationModel } from './models/integration.model';
|
|
||||||
import { AuthorityValue } from './models/authority.value';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
|
||||||
|
|
||||||
protected toCache = true;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected objectCache: ObjectCacheService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
|
||||||
const dataDefinition = this.process<IntegrationModel>(data.payload, request);
|
|
||||||
return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload));
|
|
||||||
} else {
|
|
||||||
return new ErrorResponse(
|
|
||||||
Object.assign(
|
|
||||||
new Error('Unexpected response from Integration endpoint'),
|
|
||||||
{statusCode: data.statusCode, statusText: data.statusText}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processResponse(data: PaginatedList<IntegrationModel>): any {
|
|
||||||
const returnList = Array.of();
|
|
||||||
data.page.forEach((item, index) => {
|
|
||||||
if (item.type === AuthorityValue.type.value) {
|
|
||||||
data.page[index] = Object.assign(new AuthorityValue(), item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,96 +0,0 @@
|
|||||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
|
||||||
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { IntegrationRequest } from '../data/request.models';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
|
||||||
import { IntegrationService } from './integration.service';
|
|
||||||
import { IntegrationSearchOptions } from './models/integration-options.model';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
|
||||||
|
|
||||||
const LINK_NAME = 'authorities';
|
|
||||||
const ENTRIES = 'entries';
|
|
||||||
const ENTRY_VALUE = 'entryValue';
|
|
||||||
|
|
||||||
class TestService extends IntegrationService {
|
|
||||||
protected linkPath = LINK_NAME;
|
|
||||||
protected entriesEndpoint = ENTRIES;
|
|
||||||
protected entryValueEndpoint = ENTRY_VALUE;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected requestService: RequestService,
|
|
||||||
protected rdbService: RemoteDataBuildService,
|
|
||||||
protected halService: HALEndpointService) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('IntegrationService', () => {
|
|
||||||
let scheduler: TestScheduler;
|
|
||||||
let service: TestService;
|
|
||||||
let requestService: RequestService;
|
|
||||||
let rdbService: RemoteDataBuildService;
|
|
||||||
let halService: any;
|
|
||||||
let findOptions: IntegrationSearchOptions;
|
|
||||||
|
|
||||||
const name = 'type';
|
|
||||||
const metadata = 'dc.type';
|
|
||||||
const query = '';
|
|
||||||
const value = 'test';
|
|
||||||
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
|
||||||
const integrationEndpoint = 'https://rest.api/integration';
|
|
||||||
const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`;
|
|
||||||
const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
|
||||||
const entryValueEndpoint = `${serviceEndpoint}/${name}/entryValue/${value}?metadata=${metadata}`;
|
|
||||||
|
|
||||||
findOptions = new IntegrationSearchOptions(uuid, name, metadata);
|
|
||||||
|
|
||||||
function initTestService(): TestService {
|
|
||||||
return new TestService(
|
|
||||||
requestService,
|
|
||||||
rdbService,
|
|
||||||
halService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
halService = new HALEndpointServiceStub(integrationEndpoint);
|
|
||||||
findOptions = new IntegrationSearchOptions(uuid, name, metadata, query);
|
|
||||||
service = initTestService();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEntriesByName', () => {
|
|
||||||
|
|
||||||
it('should configure a new IntegrationRequest', () => {
|
|
||||||
const expected = new IntegrationRequest(requestService.generateRequestId(), entriesEndpoint);
|
|
||||||
scheduler.schedule(() => service.getEntriesByName(findOptions).subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEntryByValue', () => {
|
|
||||||
|
|
||||||
it('should configure a new IntegrationRequest', () => {
|
|
||||||
findOptions = new IntegrationSearchOptions(
|
|
||||||
null,
|
|
||||||
name,
|
|
||||||
metadata,
|
|
||||||
value);
|
|
||||||
|
|
||||||
const expected = new IntegrationRequest(requestService.generateRequestId(), entryValueEndpoint);
|
|
||||||
scheduler.schedule(() => service.getEntryByValue(findOptions).subscribe());
|
|
||||||
scheduler.flush();
|
|
||||||
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,121 +0,0 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { IntegrationSuccessResponse } from '../cache/response.models';
|
|
||||||
import { GetRequest, IntegrationRequest } from '../data/request.models';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { IntegrationData } from './integration-data';
|
|
||||||
import { IntegrationSearchOptions } from './models/integration-options.model';
|
|
||||||
import { getResponseFromEntry } from '../shared/operators';
|
|
||||||
|
|
||||||
export abstract class IntegrationService {
|
|
||||||
protected request: IntegrationRequest;
|
|
||||||
protected abstract requestService: RequestService;
|
|
||||||
protected abstract linkPath: string;
|
|
||||||
protected abstract entriesEndpoint: string;
|
|
||||||
protected abstract entryValueEndpoint: string;
|
|
||||||
protected abstract halService: HALEndpointService;
|
|
||||||
|
|
||||||
protected getData(request: GetRequest): Observable<IntegrationData> {
|
|
||||||
return this.requestService.getByHref(request.href).pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
mergeMap((response: IntegrationSuccessResponse) => {
|
|
||||||
if (response.isSuccessful && isNotEmpty(response)) {
|
|
||||||
return observableOf(new IntegrationData(
|
|
||||||
response.pageInfo,
|
|
||||||
(response.dataDefinition) ? response.dataDefinition.page : []
|
|
||||||
));
|
|
||||||
} else if (!response.isSuccessful) {
|
|
||||||
return observableThrowError(new Error(`Couldn't retrieve the integration data`));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getEntriesHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
|
|
||||||
let result;
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
if (hasValue(options.name)) {
|
|
||||||
result = `${endpoint}/${options.name}/${this.entriesEndpoint}`;
|
|
||||||
} else {
|
|
||||||
result = endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.query)) {
|
|
||||||
args.push(`query=${options.query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.metadata)) {
|
|
||||||
args.push(`metadata=${options.metadata}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.uuid)) {
|
|
||||||
args.push(`uuid=${options.uuid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
|
||||||
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
|
||||||
args.push(`page=${options.currentPage - 1}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.elementsPerPage)) {
|
|
||||||
args.push(`size=${options.elementsPerPage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.sort)) {
|
|
||||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
result = `${result}?${args.join('&')}`;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getEntryValueHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
|
|
||||||
let result;
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
if (hasValue(options.name) && hasValue(options.query)) {
|
|
||||||
result = `${endpoint}/${options.name}/${this.entryValueEndpoint}/${options.query}`;
|
|
||||||
} else {
|
|
||||||
result = endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(options.metadata)) {
|
|
||||||
args.push(`metadata=${options.metadata}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
|
||||||
result = `${result}?${args.join('&')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEntriesByName(options: IntegrationSearchOptions): Observable<IntegrationData> {
|
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
|
||||||
map((endpoint: string) => this.getEntriesHref(endpoint, options)),
|
|
||||||
filter((href: string) => isNotEmpty(href)),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)),
|
|
||||||
tap((request: GetRequest) => this.requestService.configure(request)),
|
|
||||||
mergeMap((request: GetRequest) => this.getData(request)),
|
|
||||||
distinctUntilChanged());
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEntryByValue(options: IntegrationSearchOptions): Observable<IntegrationData> {
|
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
|
||||||
map((endpoint: string) => this.getEntryValueHref(endpoint, options)),
|
|
||||||
filter((href: string) => isNotEmpty(href)),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)),
|
|
||||||
tap((request: GetRequest) => this.requestService.configure(request)),
|
|
||||||
mergeMap((request: GetRequest) => this.getData(request)),
|
|
||||||
distinctUntilChanged());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
export class AuthorityOptions {
|
|
||||||
name: string;
|
|
||||||
metadata: string;
|
|
||||||
scope: string;
|
|
||||||
closed: boolean;
|
|
||||||
|
|
||||||
constructor(name: string,
|
|
||||||
metadata: string,
|
|
||||||
scope: string,
|
|
||||||
closed: boolean = false) {
|
|
||||||
this.name = name;
|
|
||||||
this.metadata = metadata;
|
|
||||||
this.scope = scope;
|
|
||||||
this.closed = closed;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
import { ResourceType } from '../../shared/resource-type';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The resource type for AuthorityValue
|
|
||||||
*
|
|
||||||
* Needs to be in a separate file to prevent circular
|
|
||||||
* dependencies in webpack.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const AUTHORITY_VALUE = new ResourceType('authority');
|
|
@@ -1,92 +0,0 @@
|
|||||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
|
||||||
import { OtherInformation } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
|
||||||
import { typedObject } from '../../cache/builders/build-decorators';
|
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
|
||||||
import { MetadataValueInterface } from '../../shared/metadata.models';
|
|
||||||
import { AUTHORITY_VALUE } from './authority.resource-type';
|
|
||||||
import { IntegrationModel } from './integration.model';
|
|
||||||
import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class representing an authority object
|
|
||||||
*/
|
|
||||||
@typedObject
|
|
||||||
@inheritSerialization(IntegrationModel)
|
|
||||||
export class AuthorityValue extends IntegrationModel implements MetadataValueInterface {
|
|
||||||
static type = AUTHORITY_VALUE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The identifier of this authority
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The display value of this authority
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
display: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value of this authority
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object containing additional information related to this authority
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
otherInformation: OtherInformation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The language code of this authority value
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link HALLink}s for this AuthorityValue
|
|
||||||
*/
|
|
||||||
@deserialize
|
|
||||||
_links: {
|
|
||||||
self: HALLink,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method checks if authority has an identifier value
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
hasAuthority(): boolean {
|
|
||||||
return isNotEmpty(this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method checks if authority has a value
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
hasValue(): boolean {
|
|
||||||
return isNotEmpty(this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method checks if authority has related information object
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
hasOtherInformation(): boolean {
|
|
||||||
return isNotEmpty(this.otherInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method checks if authority has a placeholder as value
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
hasPlaceholder(): boolean {
|
|
||||||
return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
import { SortOptions } from '../../cache/models/sort-options.model';
|
|
||||||
|
|
||||||
export class IntegrationSearchOptions {
|
|
||||||
|
|
||||||
constructor(public uuid: string = '',
|
|
||||||
public name: string = '',
|
|
||||||
public metadata: string = '',
|
|
||||||
public query: string = '',
|
|
||||||
public elementsPerPage?: number,
|
|
||||||
public currentPage?: number,
|
|
||||||
public sort?: SortOptions) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
import { autoserialize, deserialize } from 'cerialize';
|
|
||||||
import { CacheableObject } from '../../cache/object-cache.reducer';
|
|
||||||
import { HALLink } from '../../shared/hal-link.model';
|
|
||||||
|
|
||||||
export abstract class IntegrationModel implements CacheableObject {
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
self: string;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@autoserialize
|
|
||||||
public type: any;
|
|
||||||
|
|
||||||
@deserialize
|
|
||||||
public _links: {
|
|
||||||
self: HALLink,
|
|
||||||
[name: string]: HALLink
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,11 +1,16 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CoreState } from '../../core.reducers';
|
import { CoreState } from '../../core.reducers';
|
||||||
import { NewPatchAddOperationAction, NewPatchMoveOperationAction, NewPatchRemoveOperationAction, NewPatchReplaceOperationAction } from '../json-patch-operations.actions';
|
import {
|
||||||
|
NewPatchAddOperationAction,
|
||||||
|
NewPatchMoveOperationAction,
|
||||||
|
NewPatchRemoveOperationAction,
|
||||||
|
NewPatchReplaceOperationAction
|
||||||
|
} from '../json-patch-operations.actions';
|
||||||
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { hasNoValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { dateToISOFormat } from '../../../shared/date.util';
|
import { dateToISOFormat } from '../../../shared/date.util';
|
||||||
import { AuthorityValue } from '../../integration/models/authority.value';
|
import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model';
|
||||||
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
|
import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model';
|
||||||
|
|
||||||
@@ -96,7 +101,7 @@ export class JsonPatchOperationsBuilder {
|
|||||||
|
|
||||||
protected prepareValue(value: any, plain: boolean, first: boolean) {
|
protected prepareValue(value: any, plain: boolean, first: boolean) {
|
||||||
let operationValue: any = null;
|
let operationValue: any = null;
|
||||||
if (isNotEmpty(value)) {
|
if (hasValue(value)) {
|
||||||
if (plain) {
|
if (plain) {
|
||||||
operationValue = value;
|
operationValue = value;
|
||||||
} else {
|
} else {
|
||||||
@@ -125,10 +130,12 @@ export class JsonPatchOperationsBuilder {
|
|||||||
operationValue = value;
|
operationValue = value;
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value));
|
operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value));
|
||||||
} else if (value instanceof AuthorityValue) {
|
} else if (value instanceof VocabularyEntry) {
|
||||||
operationValue = this.prepareAuthorityValue(value);
|
operationValue = this.prepareAuthorityValue(value);
|
||||||
} else if (value instanceof FormFieldLanguageValueObject) {
|
} else if (value instanceof FormFieldLanguageValueObject) {
|
||||||
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
|
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
|
||||||
|
} else if (value.hasOwnProperty('authority')) {
|
||||||
|
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority);
|
||||||
} else if (value.hasOwnProperty('value')) {
|
} else if (value.hasOwnProperty('value')) {
|
||||||
operationValue = new FormFieldMetadataValueObject(value.value);
|
operationValue = new FormFieldMetadataValueObject(value.value);
|
||||||
} else {
|
} else {
|
||||||
@@ -144,10 +151,10 @@ export class JsonPatchOperationsBuilder {
|
|||||||
return operationValue;
|
return operationValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected prepareAuthorityValue(value: any) {
|
protected prepareAuthorityValue(value: any): FormFieldMetadataValueObject {
|
||||||
let operationValue: any = null;
|
let operationValue: FormFieldMetadataValueObject;
|
||||||
if (isNotEmpty(value.id)) {
|
if (isNotEmpty(value.authority)) {
|
||||||
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id);
|
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority);
|
||||||
} else {
|
} else {
|
||||||
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
|
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { async, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
@@ -22,7 +21,6 @@ import {
|
|||||||
StartTransactionPatchOperationsAction
|
StartTransactionPatchOperationsAction
|
||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
|
|
||||||
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||||
protected linkPath = '';
|
protected linkPath = '';
|
||||||
|
@@ -10,7 +10,7 @@ import { Observable, of as observableOf, combineLatest } from 'rxjs';
|
|||||||
import { map, take, flatMap } from 'rxjs/operators';
|
import { map, take, flatMap } from 'rxjs/operators';
|
||||||
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
|
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
|
||||||
|
|
||||||
export const LANG_COOKIE = 'language_cookie';
|
export const LANG_COOKIE = 'dsLanguage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This enum defines the possible origin of the languages
|
* This enum defines the possible origin of the languages
|
||||||
|
@@ -68,8 +68,8 @@ export class MetadataField extends ListableObject implements HALResource {
|
|||||||
schema?: Observable<RemoteData<MetadataSchema>>;
|
schema?: Observable<RemoteData<MetadataSchema>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to print this metadata field as a string
|
* Method to print this metadata field as a string without the schema
|
||||||
* @param separator The separator between the schema, element and qualifier in the string
|
* @param separator The separator between element and qualifier in the string
|
||||||
*/
|
*/
|
||||||
toString(separator: string = '.'): string {
|
toString(separator: string = '.'): string {
|
||||||
let key = this.element;
|
let key = this.element;
|
||||||
|
@@ -30,7 +30,6 @@ import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'
|
|||||||
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
|
|
||||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||||
@@ -90,20 +89,6 @@ export class RegistryService {
|
|||||||
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all existing metadata fields as a paginated list
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
// TODO this is temporarily disabled. The performance is too bad.
|
|
||||||
// It is used down the line for validation. That validation will have to be rewritten against a new rest endpoint.
|
|
||||||
// Not by downloading the list of all fields.
|
|
||||||
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
|
||||||
}
|
|
||||||
|
|
||||||
public editMetadataSchema(schema: MetadataSchema) {
|
public editMetadataSchema(schema: MetadataSchema) {
|
||||||
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
||||||
}
|
}
|
||||||
@@ -151,6 +136,7 @@ export class RegistryService {
|
|||||||
public getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
|
public getSelectedMetadataSchemas(): Observable<MetadataSchema[]> {
|
||||||
return this.store.pipe(select(selectedMetadataSchemasSelector));
|
return this.store.pipe(select(selectedMetadataSchemasSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to start editing a metadata field, dispatches an edit field action
|
* Method to start editing a metadata field, dispatches an edit field action
|
||||||
* @param field The field that's being edited
|
* @param field The field that's being edited
|
||||||
@@ -165,12 +151,14 @@ export class RegistryService {
|
|||||||
public cancelEditMetadataField() {
|
public cancelEditMetadataField() {
|
||||||
this.store.dispatch(new MetadataRegistryCancelFieldAction());
|
this.store.dispatch(new MetadataRegistryCancelFieldAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to retrieve the metadata field that are currently being edited
|
* Method to retrieve the metadata field that are currently being edited
|
||||||
*/
|
*/
|
||||||
public getActiveMetadataField(): Observable<MetadataField> {
|
public getActiveMetadataField(): Observable<MetadataField> {
|
||||||
return this.store.pipe(select(editMetadataFieldSelector));
|
return this.store.pipe(select(editMetadataFieldSelector));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to select a metadata field, dispatches a select field action
|
* Method to select a metadata field, dispatches a select field action
|
||||||
* @param field The field that's being selected
|
* @param field The field that's being selected
|
||||||
@@ -178,6 +166,7 @@ export class RegistryService {
|
|||||||
public selectMetadataField(field: MetadataField) {
|
public selectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistrySelectFieldAction(field));
|
this.store.dispatch(new MetadataRegistrySelectFieldAction(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to deselect a metadata field, dispatches a deselect field action
|
* Method to deselect a metadata field, dispatches a deselect field action
|
||||||
* @param field The field that's it being deselected
|
* @param field The field that's it being deselected
|
||||||
@@ -185,6 +174,7 @@ export class RegistryService {
|
|||||||
public deselectMetadataField(field: MetadataField) {
|
public deselectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field));
|
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +203,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
this.showNotifications(true, isUpdate, false, { prefix: schema.prefix });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -244,7 +234,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, false, true, {field: field.toString()});
|
this.showNotifications(true, false, true, { field: field.toString() });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +249,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.showNotifications(true, true, true, {field: field.toString()});
|
this.showNotifications(true, true, true, { field: field.toString() });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -271,6 +261,7 @@ export class RegistryService {
|
|||||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||||
return this.metadataFieldService.delete(`${id}`);
|
return this.metadataFieldService.delete(`${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method that clears a cached metadata field request and returns its REST url
|
* Method that clears a cached metadata field request and returns its REST url
|
||||||
*/
|
*/
|
||||||
@@ -297,13 +288,11 @@ export class RegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a filtered paginated list of metadata fields
|
* Retrieve a filtered paginated list of metadata fields
|
||||||
* @param query {string} The query to filter the field names by
|
* @param query {string} The query to use for the metadata field name, can be part of the fully qualified field,
|
||||||
|
* should start with the start of the schema, element or qualifier (e.g. “dc.ti”, “contributor”, “auth”, “contributor.ot”)
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
||||||
*/
|
*/
|
||||||
// TODO this is temporarily disabled. The performance is too bad.
|
queryMetadataFields(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
// Querying metadatafields will need to be implemented as a search endpoint on the rest api,
|
return this.metadataFieldService.searchByFieldNameParams(null, null, null, query, null, options, ...linksToFollow);
|
||||||
// not by downloading everything and preforming the query client side.
|
|
||||||
queryMetadataFields(query: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
src/app/core/reload/reload.guard.spec.ts
Normal file
47
src/app/core/reload/reload.guard.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ReloadGuard } from './reload.guard';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
describe('ReloadGuard', () => {
|
||||||
|
let guard: ReloadGuard;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
|
||||||
|
guard = new ReloadGuard(router);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let route;
|
||||||
|
|
||||||
|
describe('when the route\'s query params contain a redirect url', () => {
|
||||||
|
let redirectUrl;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
redirectUrl = '/redirect/url?param=extra';
|
||||||
|
route = {
|
||||||
|
queryParams: {
|
||||||
|
redirect: redirectUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a UrlTree with the redirect URL', () => {
|
||||||
|
guard.canActivate(route, undefined);
|
||||||
|
expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the route\'s query params doesn\'t contain a redirect url', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
route = {
|
||||||
|
queryParams: {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a UrlTree to home', () => {
|
||||||
|
guard.canActivate(route, undefined);
|
||||||
|
expect(router.createUrlTree).toHaveBeenCalledWith(['home']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
26
src/app/core/reload/reload.guard.ts
Normal file
26
src/app/core/reload/reload.guard.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A guard redirecting the user to the URL provided in the route's query params
|
||||||
|
* When no redirect url is found, the user is redirected to the homepage
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ReloadGuard implements CanActivate {
|
||||||
|
constructor(private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the UrlTree of the URL to redirect to
|
||||||
|
* @param route
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
|
||||||
|
if (isNotEmpty(route.queryParams.redirect)) {
|
||||||
|
return this.router.parseUrl(route.queryParams.redirect);
|
||||||
|
} else {
|
||||||
|
return this.router.createUrlTree(['home']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/app/core/services/browser-hard-redirect.service.spec.ts
Normal file
41
src/app/core/services/browser-hard-redirect.service.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
import {BrowserHardRedirectService} from './browser-hard-redirect.service';
|
||||||
|
|
||||||
|
describe('BrowserHardRedirectService', () => {
|
||||||
|
|
||||||
|
const mockLocation = {
|
||||||
|
href: undefined,
|
||||||
|
pathname: '/pathname',
|
||||||
|
search: '/search',
|
||||||
|
} as Location;
|
||||||
|
|
||||||
|
const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when performing a redirect', () => {
|
||||||
|
|
||||||
|
const redirect = 'test redirect';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.redirect(redirect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the location', () => {
|
||||||
|
expect(mockLocation.href).toEqual(redirect);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when requesting the current route', () => {
|
||||||
|
|
||||||
|
it('should return the location origin', () => {
|
||||||
|
expect(service.getCurrentRoute()).toEqual(mockLocation.pathname + mockLocation.search);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
35
src/app/core/services/browser-hard-redirect.service.ts
Normal file
35
src/app/core/services/browser-hard-redirect.service.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Inject, Injectable, InjectionToken } from '@angular/core';
|
||||||
|
import { HardRedirectService } from './hard-redirect.service';
|
||||||
|
|
||||||
|
export const LocationToken = new InjectionToken('Location');
|
||||||
|
|
||||||
|
export function locationProvider(): Location {
|
||||||
|
return window.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for performing hard redirects within the browser app module
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowserHardRedirectService implements HardRedirectService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(LocationToken) protected location: Location,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a hard redirect to URL
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
redirect(url: string) {
|
||||||
|
this.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin of a request
|
||||||
|
*/
|
||||||
|
getCurrentRoute() {
|
||||||
|
return this.location.pathname + this.location.search;
|
||||||
|
}
|
||||||
|
}
|
22
src/app/core/services/hard-redirect.service.ts
Normal file
22
src/app/core/services/hard-redirect.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to take care of hard redirects
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export abstract class HardRedirectService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a hard redirect to a given location.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the page to redirect to
|
||||||
|
*/
|
||||||
|
abstract redirect(url: string);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current route, with query params included
|
||||||
|
* e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
|
||||||
|
*/
|
||||||
|
abstract getCurrentRoute();
|
||||||
|
}
|
43
src/app/core/services/server-hard-redirect.service.spec.ts
Normal file
43
src/app/core/services/server-hard-redirect.service.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
||||||
|
|
||||||
|
describe('ServerHardRedirectService', () => {
|
||||||
|
|
||||||
|
const mockRequest = jasmine.createSpyObj(['get']);
|
||||||
|
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
|
||||||
|
|
||||||
|
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when performing a redirect', () => {
|
||||||
|
|
||||||
|
const redirect = 'test redirect';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.redirect(redirect);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the response object', () => {
|
||||||
|
expect(mockResponse.redirect).toHaveBeenCalledWith(302, redirect);
|
||||||
|
expect(mockResponse.end).toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when requesting the current route', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRequest.originalUrl = 'original/url';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the location origin', () => {
|
||||||
|
expect(service.getCurrentRoute()).toEqual(mockRequest.originalUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
62
src/app/core/services/server-hard-redirect.service.ts
Normal file
62
src/app/core/services/server-hard-redirect.service.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
import { HardRedirectService } from './hard-redirect.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for performing hard redirects within the server app module
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServerHardRedirectService implements HardRedirectService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(REQUEST) protected req: Request,
|
||||||
|
@Inject(RESPONSE) protected res: Response,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a hard redirect to URL
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
redirect(url: string) {
|
||||||
|
|
||||||
|
if (url === this.req.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.res.finished) {
|
||||||
|
const req: any = this.req;
|
||||||
|
req._r_count = (req._r_count || 0) + 1;
|
||||||
|
|
||||||
|
console.warn('Attempted to redirect on a finished response. From',
|
||||||
|
this.req.url, 'to', url);
|
||||||
|
|
||||||
|
if (req._r_count > 10) {
|
||||||
|
console.error('Detected a redirection loop. killing the nodejs process');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// attempt to use the already set status
|
||||||
|
let status = this.res.statusCode || 0;
|
||||||
|
if (status < 300 || status >= 400) {
|
||||||
|
// temporary redirect
|
||||||
|
status = 302;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
||||||
|
this.res.redirect(status, url);
|
||||||
|
this.res.end();
|
||||||
|
// I haven't found a way to correctly stop Angular rendering.
|
||||||
|
// So we just let it end its work, though we have already closed
|
||||||
|
// the response.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin of a request
|
||||||
|
*/
|
||||||
|
getCurrentRoute() {
|
||||||
|
return this.req.originalUrl;
|
||||||
|
}
|
||||||
|
}
|
@@ -184,4 +184,25 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
|
|||||||
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
getRenderTypes(): Array<string | GenericConstructor<ListableObject>> {
|
||||||
return [this.constructor as GenericConstructor<ListableObject>];
|
return [this.constructor as GenericConstructor<ListableObject>];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMetadata(key: string, language?: string, ...values: string[]) {
|
||||||
|
const mdValues: MetadataValue[] = values.map((value: string, index: number) => {
|
||||||
|
const md = new MetadataValue();
|
||||||
|
md.value = value;
|
||||||
|
md.authority = null;
|
||||||
|
md.confidence = -1;
|
||||||
|
md.language = language || null;
|
||||||
|
md.place = index;
|
||||||
|
return md;
|
||||||
|
});
|
||||||
|
if (hasNoValue(this.metadata)) {
|
||||||
|
this.metadata = Object.create({});
|
||||||
|
}
|
||||||
|
this.metadata[key] = mdValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMetadata(key: string) {
|
||||||
|
delete this.metadata[key];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { filter, find, flatMap, map, take, tap } from 'rxjs/operators';
|
import { filter, find, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
import { DSOSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
@@ -9,9 +9,12 @@ import { RemoteData } from '../data/remote-data';
|
|||||||
import { RestRequest } from '../data/request.models';
|
import { RestRequest } from '../data/request.models';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { BrowseDefinition } from './browse-definition.model';
|
import { BrowseDefinition } from './browse-definition.model';
|
||||||
import { DSpaceObject } from './dspace-object.model';
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
import { getUnauthorizedRoute } from '../../app-routing-paths';
|
import { getUnauthorizedRoute } from '../../app-routing-paths';
|
||||||
|
import { getEndUserAgreementPath } from '../../info/info-routing.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains custom RxJS operators that can be used in multiple places
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
@@ -192,6 +195,20 @@ export const returnUnauthorizedUrlTreeOnFalse = (router: Router) =>
|
|||||||
return authorized ? authorized : router.parseUrl(getUnauthorizedRoute())
|
return authorized ? authorized : router.parseUrl(getUnauthorizedRoute())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator that returns a UrlTree to the unauthorized page when the boolean received is false
|
||||||
|
* @param router Router
|
||||||
|
* @param redirect Redirect URL to add to the UrlTree. This is used to redirect back to the original route after the
|
||||||
|
* user accepts the agreement.
|
||||||
|
*/
|
||||||
|
export const returnEndUserAgreementUrlTreeOnFalse = (router: Router, redirect: string) =>
|
||||||
|
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
|
||||||
|
source.pipe(
|
||||||
|
map((hasAgreed: boolean) => {
|
||||||
|
const queryParams = { redirect: encodeURIComponent(redirect) };
|
||||||
|
return hasAgreed ? hasAgreed : router.createUrlTree([getEndUserAgreementPath()], { queryParams });
|
||||||
|
}));
|
||||||
|
|
||||||
export const getFinishedRemoteData = () =>
|
export const getFinishedRemoteData = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
||||||
@@ -250,3 +267,27 @@ export const paginatedListToArray = () =>
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
map((objectRD: RemoteData<PaginatedList<T>>) => objectRD.payload.page.filter((object: T) => hasValue(object)))
|
map((objectRD: RemoteData<PaginatedList<T>>) => objectRD.payload.page.filter((object: T) => hasValue(object)))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a list of metadata fields into an array of string representing their schema.element.qualifier string
|
||||||
|
*/
|
||||||
|
export const metadataFieldsToString = () =>
|
||||||
|
(source: Observable<RemoteData<PaginatedList<MetadataField>>>): Observable<string[]> =>
|
||||||
|
source.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
map((fieldRD: RemoteData<PaginatedList<MetadataField>>) => {
|
||||||
|
return fieldRD.payload.page.filter((object: MetadataField) => hasValue(object))
|
||||||
|
}),
|
||||||
|
switchMap((fields: MetadataField[]) => {
|
||||||
|
const fieldSchemaArray = fields.map((field: MetadataField) => {
|
||||||
|
return field.schema.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((schema: MetadataSchema) => ({ field, schema }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return observableCombineLatest(fieldSchemaArray);
|
||||||
|
}),
|
||||||
|
map((fieldSchemaArray: Array<{ field: MetadataField, schema: MetadataSchema }>): string[] => {
|
||||||
|
return fieldSchemaArray.map((fieldSchema: { field: MetadataField, schema: MetadataSchema }) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import { Observable } from 'rxjs';
|
|
||||||
import { SubmissionService } from '../../submission/submission.service';
|
import { SubmissionService } from '../../submission/submission.service';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { SubmissionObject } from './models/submission-object.model';
|
import { SubmissionObject } from './models/submission-object.model';
|
||||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
|
||||||
import { SubmissionObjectDataService } from './submission-object-data.service';
|
import { SubmissionObjectDataService } from './submission-object-data.service';
|
||||||
import { SubmissionScopeType } from './submission-scope-type';
|
import { SubmissionScopeType } from './submission-scope-type';
|
||||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { deepClone } from 'fast-json-patch';
|
import { deepClone } from 'fast-json-patch';
|
||||||
import { DSOResponseParsingService } from '../data/dso-response-parsing.service';
|
import { DSOResponseParsingService } from '../data/dso-response-parsing.service';
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
|||||||
return new ErrorResponse(
|
return new ErrorResponse(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
new Error('Unexpected response from server'),
|
new Error('Unexpected response from server'),
|
||||||
{statusCode: data.statusCode, statusText: data.statusText}
|
{ statusCode: data.statusCode, statusText: data.statusText }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
|||||||
|
|
||||||
processedList.forEach((item) => {
|
processedList.forEach((item) => {
|
||||||
|
|
||||||
item = Object.assign({}, item);
|
// item = Object.assign({}, item);
|
||||||
// In case data is an Instance of WorkspaceItem normalize field value of all the section of type form
|
// In case data is an Instance of WorkspaceItem normalize field value of all the section of type form
|
||||||
if (item instanceof WorkspaceItem
|
if (item instanceof WorkspaceItem
|
||||||
|| item instanceof WorkflowItem) {
|
|| item instanceof WorkflowItem) {
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { ResourceType } from '../../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for vocabulary models
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VOCABULARY = new ResourceType('vocabulary');
|
||||||
|
export const VOCABULARY_ENTRY = new ResourceType('vocabularyEntry');
|
||||||
|
export const VOCABULARY_ENTRY_DETAIL = new ResourceType('vocabularyEntryDetail');
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user