mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into iiif-mirador
This commit is contained in:
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -16,6 +16,9 @@ jobs:
|
|||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: '/server'
|
DSPACE_REST_NAMESPACE: '/server'
|
||||||
DSPACE_REST_SSL: false
|
DSPACE_REST_SSL: false
|
||||||
|
# When Chrome version is specified, we pin to a specific version of Chrome & ChromeDriver
|
||||||
|
# Comment this out to use the latest release of both.
|
||||||
|
CHROME_VERSION: "90.0.4430.212-1"
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
@@ -34,10 +37,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Install latest Chrome (for e2e tests)
|
# If CHROME_VERSION env variable specified above, then pin to that version.
|
||||||
|
# Otherwise, just install latest version of Chrome.
|
||||||
|
- name: Install Chrome (for e2e tests)
|
||||||
run: |
|
run: |
|
||||||
|
if [[ -z "${CHROME_VERSION}" ]]
|
||||||
|
then
|
||||||
|
echo "Installing latest stable version"
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get --only-upgrade install google-chrome-stable -y
|
sudo apt-get --only-upgrade install google-chrome-stable -y
|
||||||
|
else
|
||||||
|
echo "Installing version ${CHROME_VERSION}"
|
||||||
|
wget -q "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||||
|
sudo dpkg -i "google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||||
|
fi
|
||||||
google-chrome --version
|
google-chrome --version
|
||||||
|
|
||||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||||
@@ -53,8 +66,11 @@ jobs:
|
|||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: ${{ runner.os }}-yarn-
|
restore-keys: ${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install the latest chromedriver compatible with the installed chrome version
|
- name: Install latest ChromeDriver compatible with installed Chrome
|
||||||
run: yarn global add chromedriver --detect_chromedriver_version
|
# needs to be npm, the --detect_chromedriver_version flag doesn't work with yarn global
|
||||||
|
run: |
|
||||||
|
npm install -g chromedriver --detect_chromedriver_version
|
||||||
|
chromedriver -v
|
||||||
|
|
||||||
- name: Install Yarn dependencies
|
- name: Install Yarn dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
For information regarding which versions of DSpace are currently under support, please see our DSpace Software Support Policy:
|
||||||
|
|
||||||
|
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in a supported version of DSpace, we encourage you to let us know right away.
|
||||||
|
We will investigate all legitimate reports and do our best to quickly fix the problem. Please see our DSpace Software Support Policy
|
||||||
|
for information on privately reporting vulnerabilities:
|
||||||
|
|
||||||
|
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
@@ -47,6 +47,7 @@
|
|||||||
"src/robots.txt"
|
"src/robots.txt"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"src/styles/startup.scss",
|
||||||
{
|
{
|
||||||
"input": "src/styles/base-theme.scss",
|
"input": "src/styles/base-theme.scss",
|
||||||
"inject": false,
|
"inject": false,
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
# Docker Compose files
|
# Docker Compose files
|
||||||
|
|
||||||
|
***
|
||||||
|
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
|
||||||
|
***
|
||||||
|
|
||||||
## docker directory
|
## docker directory
|
||||||
- docker-compose.yml
|
- docker-compose.yml
|
||||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||||
|
@@ -69,7 +69,6 @@ exports.config = {
|
|||||||
plugins: [{
|
plugins: [{
|
||||||
path: '../node_modules/protractor-istanbul-plugin'
|
path: '../node_modules/protractor-istanbul-plugin'
|
||||||
}],
|
}],
|
||||||
|
|
||||||
framework: 'jasmine',
|
framework: 'jasmine',
|
||||||
jasmineNodeOpts: {
|
jasmineNodeOpts: {
|
||||||
showColors: true,
|
showColors: true,
|
||||||
@@ -85,7 +84,7 @@ exports.config = {
|
|||||||
onPrepare: function () {
|
onPrepare: function () {
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({
|
jasmine.getEnv().addReporter(new SpecReporter({
|
||||||
spec: {
|
spec: {
|
||||||
displayStacktrace: true
|
displayStacktrace: 'pretty'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
36
e2e/src/item-statistics/item-statistics.e2e-spec.ts
Normal file
36
e2e/src/item-statistics/item-statistics.e2e-spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ProtractorPage } from './item-statistics.po';
|
||||||
|
import { browser } from 'protractor';
|
||||||
|
import { UIURLCombiner } from '../../../src/app/core/url-combiner/ui-url-combiner';
|
||||||
|
|
||||||
|
describe('protractor Item statics', () => {
|
||||||
|
let page: ProtractorPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new ProtractorPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain element ds-item-page when navigating when navigating to an item page', () => {
|
||||||
|
page.navigateToItemPage();
|
||||||
|
expect<any>(page.elementTagExists('ds-item-page')).toEqual(true);
|
||||||
|
expect<any>(page.elementTagExists('ds-item-statistics-page')).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
|
page.navigateToItemPage();
|
||||||
|
expect(browser.getCurrentUrl()).toEqual(new UIURLCombiner(page.ENTITYPAGE).toString());
|
||||||
|
expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMSTATISTICSPAGE).toString());
|
||||||
|
expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMPAGE).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain element ds-item-statistics-page when navigating when navigating to an item statistics page', () => {
|
||||||
|
page.navigateToItemStatisticsPage();
|
||||||
|
expect<any>(page.elementTagExists('ds-item-statistics-page')).toEqual(true);
|
||||||
|
expect<any>(page.elementTagExists('ds-item-page')).toEqual(false);
|
||||||
|
});
|
||||||
|
it('should contain the item statistics page url when navigating to an item statistics page', () => {
|
||||||
|
page.navigateToItemStatisticsPage();
|
||||||
|
expect(browser.getCurrentUrl()).toEqual(new UIURLCombiner(page.ITEMSTATISTICSPAGE).toString());
|
||||||
|
expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ENTITYPAGE).toString());
|
||||||
|
expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMPAGE).toString());
|
||||||
|
});
|
||||||
|
});
|
18
e2e/src/item-statistics/item-statistics.po.ts
Normal file
18
e2e/src/item-statistics/item-statistics.po.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { browser, element, by } from 'protractor';
|
||||||
|
|
||||||
|
export class ProtractorPage {
|
||||||
|
ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
ITEMSTATISTICSPAGE = '/statistics/items/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
|
||||||
|
navigateToItemPage() {
|
||||||
|
return browser.get(this.ITEMPAGE);
|
||||||
|
}
|
||||||
|
navigateToItemStatisticsPage() {
|
||||||
|
return browser.get(this.ITEMSTATISTICSPAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
elementTagExists(tag: string) {
|
||||||
|
return element(by.tagName(tag)).isPresent();
|
||||||
|
}
|
||||||
|
}
|
@@ -61,7 +61,8 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5",
|
||||||
|
"webdriver-manager": "^12.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~10.2.3",
|
"@angular/animations": "~10.2.3",
|
||||||
|
@@ -6,10 +6,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
|
||||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
@@ -22,12 +19,9 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
let comp: MetadataImportPageComponent;
|
let comp: MetadataImportPageComponent;
|
||||||
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let scriptService: any;
|
let scriptService: any;
|
||||||
let router;
|
let router;
|
||||||
let authService;
|
|
||||||
let locationStub;
|
let locationStub;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -37,13 +31,6 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
user = Object.assign(new EPerson(), {
|
|
||||||
id: 'userId',
|
|
||||||
email: 'user@test.com'
|
|
||||||
});
|
|
||||||
authService = jasmine.createSpyObj('authService', {
|
|
||||||
getAuthenticatedUserFromStore: observableOf(user)
|
|
||||||
});
|
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
});
|
});
|
||||||
@@ -65,7 +52,6 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
{ provide: NotificationsService, useValue: notificationService },
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
{ provide: ScriptDataService, useValue: scriptService },
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: AuthService, useValue: authService },
|
|
||||||
{ provide: Location, useValue: locationStub },
|
{ provide: Location, useValue: locationStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -107,9 +93,8 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
proceed.click();
|
proceed.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
|
it('metadata-import script is invoked with -f fileName and the mockFile', () => {
|
||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
|
|
||||||
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||||
];
|
];
|
||||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
@@ -23,20 +23,14 @@ import { getProcessDetailRoute } from '../../process-page/process-page-routing.p
|
|||||||
/**
|
/**
|
||||||
* Component that represents a metadata import page for administrators
|
* Component that represents a metadata import page for administrators
|
||||||
*/
|
*/
|
||||||
export class MetadataImportPageComponent implements OnInit {
|
export class MetadataImportPageComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current value of the file
|
* The current value of the file
|
||||||
*/
|
*/
|
||||||
fileObject: File;
|
fileObject: File;
|
||||||
|
|
||||||
/**
|
public constructor(private location: Location,
|
||||||
* The authenticated user's email
|
|
||||||
*/
|
|
||||||
private currentUserEmail$: Observable<string>;
|
|
||||||
|
|
||||||
public constructor(protected authService: AuthService,
|
|
||||||
private location: Location,
|
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
private scriptDataService: ScriptDataService,
|
private scriptDataService: ScriptDataService,
|
||||||
@@ -51,15 +45,6 @@ export class MetadataImportPageComponent implements OnInit {
|
|||||||
this.fileObject = file;
|
this.fileObject = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method provided by Angular. Invoked after the constructor.
|
|
||||||
*/
|
|
||||||
ngOnInit() {
|
|
||||||
this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
|
||||||
map((user: EPerson) => user.email)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When return button is pressed go to previous location
|
* When return button is pressed go to previous location
|
||||||
*/
|
*/
|
||||||
@@ -68,22 +53,17 @@ export class MetadataImportPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
|
* Starts import-metadata script with -f fileName (and the selected file)
|
||||||
*/
|
*/
|
||||||
public importMetadata() {
|
public importMetadata() {
|
||||||
if (this.fileObject == null) {
|
if (this.fileObject == null) {
|
||||||
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||||
} else {
|
} else {
|
||||||
this.currentUserEmail$.pipe(
|
|
||||||
switchMap((email: string) => {
|
|
||||||
if (isNotEmpty(email)) {
|
|
||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-e', value: email }),
|
|
||||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||||
];
|
];
|
||||||
return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]);
|
|
||||||
}
|
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
}),
|
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
).subscribe((rd: RemoteData<Process>) => {
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
<li class="sidebar-section">
|
<li class="sidebar-section">
|
||||||
<a class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
<a href="javascript:void(0);" class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="sidebar-collapsible">
|
<div class="sidebar-collapsible">
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||||
<ng-container
|
<a class="nav-item nav-link" tabindex="-1" [routerLink]="itemModel.link">{{itemModel.text | translate}}</a>
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@@ -10,14 +10,14 @@
|
|||||||
<div class="sidebar-top-level-items">
|
<div class="sidebar-top-level-items">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="admin-menu-header sidebar-section">
|
<li class="admin-menu-header sidebar-section">
|
||||||
<a class="shortcut-icon navbar-brand mr-0" href="#">
|
<a class="shortcut-icon navbar-brand mr-0" href="javascript:void(0);">
|
||||||
<span class="logo-wrapper">
|
<span class="logo-wrapper">
|
||||||
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
||||||
[alt]="('menu.header.image.logo') | translate">
|
[alt]="('menu.header.image.logo') | translate">
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="sidebar-collapsible">
|
<div class="sidebar-collapsible">
|
||||||
<a class="navbar-brand mr-0" href="#">
|
<a class="navbar-brand mr-0" href="javascript:void(0);">
|
||||||
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
|
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
|
||||||
translate}}</h4>
|
translate}}</h4>
|
||||||
</a>
|
</a>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="navbar-nav">
|
<div class="navbar-nav">
|
||||||
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
||||||
<a class="nav-item nav-link shortcut-icon"
|
<a class="nav-item nav-link shortcut-icon"
|
||||||
href="#"
|
href="javascript:void(0);"
|
||||||
(click)="toggle($event)">
|
(click)="toggle($event)">
|
||||||
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||||
[title]="'menu.section.icon.pin' | translate"></i>
|
[title]="'menu.section.icon.pin' | translate"></i>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="sidebar-collapsible">
|
<div class="sidebar-collapsible">
|
||||||
<a class="nav-item nav-link sidebar-section"
|
<a class="nav-item nav-link sidebar-section"
|
||||||
href="#"
|
href="javascript:void(0);"
|
||||||
(click)="toggle($event)">
|
(click)="toggle($event)">
|
||||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||||
|
@@ -531,14 +531,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
* Create menu sections dependent on whether or not the current user can manage access control groups
|
||||||
*/
|
*/
|
||||||
createAccessControlMenuSections() {
|
createAccessControlMenuSections() {
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups).subscribe((authorized) => {
|
observableCombineLatest(
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
|
||||||
|
).subscribe(([isSiteAdmin, canManageGroups]) => {
|
||||||
const menuList = [
|
const menuList = [
|
||||||
/* Access Control */
|
/* Access Control */
|
||||||
{
|
{
|
||||||
id: 'access_control_people',
|
id: 'access_control_people',
|
||||||
parentID: 'access_control',
|
parentID: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: authorized,
|
visible: isSiteAdmin,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_people',
|
text: 'menu.section.access_control_people',
|
||||||
@@ -549,7 +552,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'access_control_groups',
|
id: 'access_control_groups',
|
||||||
parentID: 'access_control',
|
parentID: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: authorized,
|
visible: canManageGroups,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_groups',
|
text: 'menu.section.access_control_groups',
|
||||||
@@ -571,7 +574,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
id: 'access_control',
|
id: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: authorized,
|
visible: canManageGroups || isSiteAdmin,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.TEXT,
|
type: MenuItemType.TEXT,
|
||||||
text: 'menu.section.access_control'
|
text: 'menu.section.access_control'
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||||
params: {endColor: (sidebarActiveBg | async)}}">
|
params: {endColor: (sidebarActiveBg | async)}}">
|
||||||
<div class="icon-wrapper">
|
<div class="icon-wrapper">
|
||||||
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="#">
|
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="javascript:void(0);">
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-collapsible">
|
<div class="sidebar-collapsible">
|
||||||
<a class="nav-item nav-link" href="#"
|
<a class="nav-item nav-link" href="javascript:void(0);" tabindex="-1"
|
||||||
(click)="toggleSection($event)">
|
(click)="toggleSection($event)">
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||||
<ng-container
|
<ng-container
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="container">
|
||||||
|
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="(dsoRD$ | async)?.payload?.id"></ds-resource-policies>
|
||||||
|
<div class="button-row bottom">
|
||||||
|
<div class="text-right">
|
||||||
|
<a [routerLink]="['/bitstreams', (dsoRD$ | async)?.payload?.id, 'edit']" role="button" class="btn btn-outline-secondary mr-1">
|
||||||
|
<i class="fas fa-arrow-left"></i> {{'bitstream.edit.return' | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations.component';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('BitstreamAuthorizationsComponent', () => {
|
||||||
|
let comp: BitstreamAuthorizationsComponent<DSpaceObject>;
|
||||||
|
let fixture: ComponentFixture<BitstreamAuthorizationsComponent<any>>;
|
||||||
|
|
||||||
|
const bitstream = Object.assign(new Bitstream(), {
|
||||||
|
sizeBytes: 10000,
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: 'file name',
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
content: { href: 'file-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bitstreamRD = createSuccessfulRemoteDataObject(bitstream);
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
bitstream: bitstreamRD
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [BitstreamAuthorizationsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
|
ChangeDetectorRef,
|
||||||
|
BitstreamAuthorizationsComponent,
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BitstreamAuthorizationsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
comp = null;
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should init dso remote data properly', (done) => {
|
||||||
|
const expected = cold('(a|)', { a: bitstreamRD });
|
||||||
|
expect(comp.dsoRD$).toBeObservable(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-authorizations',
|
||||||
|
templateUrl: './bitstream-authorizations.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component that handles the Collection Authorizations
|
||||||
|
*/
|
||||||
|
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial DSO object
|
||||||
|
*/
|
||||||
|
public dsoRD$: Observable<RemoteData<TDomain>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {ActivatedRoute} route
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component, setting up the collection
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.bitstream));
|
||||||
|
}
|
||||||
|
}
|
@@ -4,8 +4,14 @@ import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream
|
|||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
||||||
|
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||||
|
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
|
||||||
|
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
|
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
|
|
||||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||||
|
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module to help navigate Bitstream pages
|
* Routing module to help navigate Bitstream pages
|
||||||
@@ -27,6 +33,36 @@ const EDIT_BITSTREAM_PATH = ':id/edit';
|
|||||||
bitstream: BitstreamPageResolver
|
bitstream: BitstreamPageResolver
|
||||||
},
|
},
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH,
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicyTarget: ResourcePolicyTargetResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyCreateComponent,
|
||||||
|
data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
resolve: {
|
||||||
|
resourcePolicy: ResourcePolicyResolver
|
||||||
|
},
|
||||||
|
component: ResourcePolicyEditComponent,
|
||||||
|
data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
resolve: {
|
||||||
|
bitstream: BitstreamPageResolver
|
||||||
|
},
|
||||||
|
component: BitstreamAuthorizationsComponent,
|
||||||
|
data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||||
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
||||||
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module handles all components that are necessary for Bitstream related pages
|
* This module handles all components that are necessary for Bitstream related pages
|
||||||
@@ -14,6 +15,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
|||||||
BitstreamPageRoutingModule
|
BitstreamPageRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
BitstreamAuthorizationsComponent,
|
||||||
EditBitstreamPageComponent
|
EditBitstreamPageComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -35,7 +35,7 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
|
|||||||
*/
|
*/
|
||||||
get followLinks(): FollowLinkConfig<Bitstream>[] {
|
get followLinks(): FollowLinkConfig<Bitstream>[] {
|
||||||
return [
|
return [
|
||||||
followLink('bundle', undefined, true, true, true, followLink('item')),
|
followLink('bundle', {}, followLink('item')),
|
||||||
followLink('format')
|
followLink('format')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,11 @@
|
|||||||
[submitLabel]="'form.save'"
|
[submitLabel]="'form.save'"
|
||||||
(submitForm)="onSubmit()"
|
(submitForm)="onSubmit()"
|
||||||
(cancel)="onCancel()"
|
(cancel)="onCancel()"
|
||||||
(dfChange)="onChange($event)"></ds-form>
|
(dfChange)="onChange($event)">
|
||||||
|
<div additional class="container py-3">
|
||||||
|
<a [routerLink]="['/bitstreams', bitstreamRD?.payload?.id, 'authorizations']">{{'bitstream.edit.authorizations.link' | translate}}</a>
|
||||||
|
</div>
|
||||||
|
</ds-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||||
|
@@ -18,12 +18,8 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import {
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
createSuccessfulRemoteDataObject,
|
import { getEntityEditRoute } from '../../+item-page/item-page-routing-paths';
|
||||||
createSuccessfulRemoteDataObject$
|
|
||||||
} from '../../shared/remote-data.utils';
|
|
||||||
import { RouterStub } from '../../shared/testing/router.stub';
|
|
||||||
import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
|
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
@@ -39,7 +35,6 @@ let bitstream: Bitstream;
|
|||||||
let selectedFormat: BitstreamFormat;
|
let selectedFormat: BitstreamFormat;
|
||||||
let allFormats: BitstreamFormat[];
|
let allFormats: BitstreamFormat[];
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let routerStub;
|
|
||||||
|
|
||||||
describe('EditBitstreamPageComponent', () => {
|
describe('EditBitstreamPageComponent', () => {
|
||||||
let comp: EditBitstreamPageComponent;
|
let comp: EditBitstreamPageComponent;
|
||||||
@@ -129,10 +124,6 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemPageUrl = `fake-url/some-uuid`;
|
|
||||||
routerStub = Object.assign(new RouterStub(), {
|
|
||||||
url: `${itemPageUrl}`
|
|
||||||
});
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||||
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
|
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
|
||||||
@@ -142,7 +133,6 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamService },
|
{ provide: BitstreamDataService, useValue: bitstreamService },
|
||||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||||
{ provide: Router, useValue: routerStub },
|
|
||||||
ChangeDetectorRef
|
ChangeDetectorRef
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -154,7 +144,8 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
fixture = TestBed.createComponent(EditBitstreamPageComponent);
|
fixture = TestBed.createComponent(EditBitstreamPageComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
router = (comp as any).router;
|
router = TestBed.inject(Router);
|
||||||
|
spyOn(router, 'navigate');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on startup', () => {
|
describe('on startup', () => {
|
||||||
@@ -241,14 +232,14 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
|
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
|
||||||
comp.itemId = 'some-uuid1';
|
comp.itemId = 'some-uuid1';
|
||||||
comp.navigateToItemEditBitstreams();
|
comp.navigateToItemEditBitstreams();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
|
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
|
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
|
||||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
|
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
|
||||||
comp.itemId = undefined;
|
comp.itemId = undefined;
|
||||||
comp.navigateToItemEditBitstreams();
|
comp.navigateToItemEditBitstreams();
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
|
expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,10 +19,10 @@ import { cloneDeep } from 'lodash';
|
|||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstCompletedRemoteData,
|
||||||
getRemoteDataPayload,
|
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||||
@@ -131,15 +131,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
rows: 10
|
rows: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The Dynamic Input Model for the file's embargo (disabled on this page)
|
|
||||||
*/
|
|
||||||
embargoModel = new DynamicInputModel({
|
|
||||||
id: 'embargo',
|
|
||||||
name: 'embargo',
|
|
||||||
disabled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the selected format
|
* The Dynamic Input Model for the selected format
|
||||||
*/
|
*/
|
||||||
@@ -159,7 +150,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* All input models in a simple array for easier iterations
|
* All input models in a simple array for easier iterations
|
||||||
*/
|
*/
|
||||||
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel, this.selectedFormatModel, this.newFormatModel];
|
inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dynamic form fields used for editing the information of a bitstream
|
* The dynamic form fields used for editing the information of a bitstream
|
||||||
@@ -179,12 +170,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
this.descriptionModel
|
this.descriptionModel
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
new DynamicFormGroupModel({
|
|
||||||
id: 'embargoContainer',
|
|
||||||
group: [
|
|
||||||
this.embargoModel
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new DynamicFormGroupModel({
|
new DynamicFormGroupModel({
|
||||||
id: 'formatContainer',
|
id: 'formatContainer',
|
||||||
group: [
|
group: [
|
||||||
@@ -243,11 +228,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
host: 'row'
|
host: 'row'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
embargoContainer: {
|
|
||||||
grid: {
|
|
||||||
host: 'row'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatContainer: {
|
formatContainer: {
|
||||||
grid: {
|
grid: {
|
||||||
host: 'row'
|
host: 'row'
|
||||||
|
@@ -167,7 +167,6 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
* @param value The value of the browse-entry to display items for
|
* @param value The value of the browse-entry to display items for
|
||||||
*/
|
*/
|
||||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||||
console.log('updatePAge', searchOptions);
|
|
||||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
Subject
|
||||||
|
} from 'rxjs';
|
||||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
@@ -8,8 +13,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
|||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
|
||||||
import { MetadataService } from '../core/metadata/metadata.service';
|
|
||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
@@ -65,7 +68,6 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private metadata: MetadataService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@@ -122,10 +124,6 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((collection) => getCollectionPageRoute(collection.id))
|
map((collection) => getCollectionPageRoute(collection.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
|
||||||
this.metadata.processRemoteData(this.collectionRD$);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isNotEmpty(object: any) {
|
isNotEmpty(object: any) {
|
||||||
|
@@ -14,7 +14,7 @@ import { ResolvedAction } from '../core/resolving/resolver.actions';
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
followLink('parentCommunity', {},
|
||||||
followLink('parentCommunity')
|
followLink('parentCommunity')
|
||||||
),
|
),
|
||||||
followLink('logo')
|
followLink('logo')
|
||||||
|
@@ -2,15 +2,16 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||||
<div class="col-12 pb-4">
|
<div class="col-12 pb-4">
|
||||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
|
||||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'collection.delete.processing' | translate}}</span>
|
||||||
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,11 +6,12 @@
|
|||||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
<button class="btn btn-outline-secondary" (click)="onCancel(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)" [disabled]="(processing$ | async)">
|
||||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
<span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'community.delete.processing' | translate}}</span>
|
||||||
|
<span *ngIf="!(processing$ | async)"><i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,6 +20,7 @@ import { ThemedHomePageComponent } from './themed-home-page.component';
|
|||||||
id: 'statistics_site',
|
id: 'statistics_site',
|
||||||
active: true,
|
active: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
index: 2,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.statistics',
|
text: 'menu.section.statistics',
|
||||||
|
@@ -15,9 +15,9 @@ import { RemoteData } from '../../../core/data/remote-data';
|
|||||||
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
|
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver';
|
|
||||||
import { getAllSucceededRemoteData } from '../../../core/shared/operators';
|
import { getAllSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item.resolver';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-abstract-item-update',
|
selector: 'ds-abstract-item-update',
|
||||||
|
@@ -4,10 +4,9 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -15,7 +14,6 @@ import { LinkService } from '../../../core/cache/builders/link.service';
|
|||||||
import { Bundle } from '../../../core/shared/bundle.model';
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a bundle's bitstream map entry
|
* Interface for a bundle's bitstream map entry
|
||||||
@@ -79,7 +77,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
map((item: Item) => this.linkService.resolveLink(
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
item,
|
item,
|
||||||
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams'))
|
followLink('bundles', {}, followLink('bitstreams'))
|
||||||
))
|
))
|
||||||
) as Observable<Item>;
|
) as Observable<Item>;
|
||||||
|
|
||||||
|
@@ -5,19 +5,16 @@
|
|||||||
<p>{{'item.edit.move.description' | translate}}</p>
|
<p>{{'item.edit.move.description' | translate}}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ds-dso-input-suggestions #f id="search-form"
|
<div class="card mb-3">
|
||||||
[suggestions]="(collectionSearchResults | async)"
|
<div class="card-header">{{'dso-selector.placeholder' | translate: { type: 'collection' } }}</div>
|
||||||
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
<div class="card-body">
|
||||||
[action]="getCurrentUrl()"
|
<ds-authorized-collection-selector [types]="COLLECTIONS"
|
||||||
[name]="'item-move'"
|
[currentDSOId]="selectedCollection ? selectedCollection.id : originalCollection.id"
|
||||||
[(ngModel)]="selectedCollectionName"
|
(onSelect)="selectDso($event)">
|
||||||
(clickSuggestion)="onClick($event)"
|
</ds-authorized-collection-selector>
|
||||||
(typeSuggestion)="resetCollection($event)"
|
</div>
|
||||||
(findSuggestions)="findSuggestions($event)"
|
<div></div>
|
||||||
(click)="f.open()"
|
</div>
|
||||||
ngDefaultControl>
|
|
||||||
</ds-dso-input-suggestions>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -33,16 +30,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
<div class="button-row bottom">
|
||||||
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
<div class="float-right">
|
||||||
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
<button [routerLink]="[(itemPageRoute$ | async), 'edit']" class="btn btn-outline-secondary">
|
||||||
{{'item.edit.move.processing' | translate}}
|
<i class="fas fa-arrow-left"></i> {{'item.edit.move.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mr-0" [disabled]="!canMove" (click)="moveToCollection()">
|
||||||
|
<span *ngIf="!processing">
|
||||||
|
<i class="fas fa-save"></i> {{'item.edit.move.save-button' | translate}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="processing">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i> {{'item.edit.move.processing' | translate}}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
<button class="btn btn-danger" [disabled]="!canSubmit" (click)="discard()">
|
||||||
class="btn btn-outline-secondary">
|
<i class="fas fa-times"></i> {{"item.edit.move.discard-button" | translate}}
|
||||||
{{'item.edit.move.cancel' | translate}}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -21,6 +21,8 @@ import {
|
|||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
|
||||||
|
|
||||||
describe('ItemMoveComponent', () => {
|
describe('ItemMoveComponent', () => {
|
||||||
let comp: ItemMoveComponent;
|
let comp: ItemMoveComponent;
|
||||||
@@ -47,18 +49,25 @@ describe('ItemMoveComponent', () => {
|
|||||||
name: 'Test collection 2'
|
name: 'Test collection 2'
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataService = jasmine.createSpyObj({
|
let itemDataService;
|
||||||
moveToCollection: createSuccessfulRemoteDataObject$(collection1)
|
|
||||||
|
const mockItemDataServiceSuccess = jasmine.createSpyObj({
|
||||||
|
moveToCollection: createSuccessfulRemoteDataObject$(collection1),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockItemDataServiceFail = jasmine.createSpyObj({
|
const mockItemDataServiceFail = jasmine.createSpyObj({
|
||||||
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500)
|
moveToCollection: createFailedRemoteDataObject$('Internal server error', 500),
|
||||||
|
findById: createSuccessfulRemoteDataObject$(mockItem),
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
|
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
|
||||||
id: 'item1'
|
id: 'item1',
|
||||||
|
owningCollection: createSuccessfulRemoteDataObject$(Object.assign(new Collection(), {
|
||||||
|
id: 'originalOwningCollection',
|
||||||
|
}))
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -79,8 +88,9 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
const notificationsServiceStub = new NotificationsServiceStub();
|
const notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
describe('ItemMoveComponent success', () => {
|
const init = (mockItemDataService) => {
|
||||||
beforeEach(() => {
|
itemDataService = mockItemDataService;
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [ItemMoveComponent],
|
declarations: [ItemMoveComponent],
|
||||||
@@ -90,6 +100,7 @@ describe('ItemMoveComponent', () => {
|
|||||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
{ provide: SearchService, useValue: mockSearchService },
|
{ provide: SearchService, useValue: mockSearchService },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -97,25 +108,20 @@ describe('ItemMoveComponent', () => {
|
|||||||
fixture = TestBed.createComponent(ItemMoveComponent);
|
fixture = TestBed.createComponent(ItemMoveComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
};
|
||||||
it('should load suggestions', () => {
|
|
||||||
const expected = [
|
|
||||||
collection1,
|
|
||||||
collection2
|
|
||||||
];
|
|
||||||
|
|
||||||
comp.collectionSearchResults.subscribe((value) => {
|
describe('ItemMoveComponent success', () => {
|
||||||
expect(value).toEqual(expected);
|
beforeEach(() => {
|
||||||
}
|
init(mockItemDataServiceSuccess);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get current url ', () => {
|
it('should get current url ', () => {
|
||||||
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
||||||
});
|
});
|
||||||
it('should on click select the correct collection name and id', () => {
|
it('should select the correct collection name and id on click', () => {
|
||||||
const data = collection1;
|
const data = collection1;
|
||||||
|
|
||||||
comp.onClick(data);
|
comp.selectDso(data);
|
||||||
|
|
||||||
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
||||||
expect(comp.selectedCollection).toEqual(collection1);
|
expect(comp.selectedCollection).toEqual(collection1);
|
||||||
@@ -128,12 +134,12 @@ describe('ItemMoveComponent', () => {
|
|||||||
});
|
});
|
||||||
comp.selectedCollectionName = 'selected-collection-id';
|
comp.selectedCollectionName = 'selected-collection-id';
|
||||||
comp.selectedCollection = collection1;
|
comp.selectedCollection = collection1;
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
||||||
});
|
});
|
||||||
it('should call notificationsService success message on success', () => {
|
it('should call notificationsService success message on success', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -142,26 +148,11 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
describe('ItemMoveComponent fail', () => {
|
describe('ItemMoveComponent fail', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
init(mockItemDataServiceFail);
|
||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
|
||||||
declarations: [ItemMoveComponent],
|
|
||||||
providers: [
|
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
|
||||||
{ provide: Router, useValue: routerStub },
|
|
||||||
{ provide: ItemDataService, useValue: mockItemDataServiceFail },
|
|
||||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
|
||||||
{ provide: SearchService, useValue: mockSearchService },
|
|
||||||
], schemas: [
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
fixture = TestBed.createComponent(ItemMoveComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call notificationsService error message on fail', () => {
|
it('should call notificationsService error message on fail', () => {
|
||||||
comp.moveCollection();
|
comp.moveToCollection();
|
||||||
|
|
||||||
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
expect(notificationsServiceStub.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@@ -1,25 +1,21 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
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 {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
|
||||||
getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SearchService } from '../../../core/shared/search/search.service';
|
import { SearchService } from '../../../core/shared/search/search.service';
|
||||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
|
||||||
import { SearchResult } from '../../../shared/search/search-result.model';
|
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-move',
|
selector: 'ds-item-move',
|
||||||
@@ -38,7 +34,8 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
|
|
||||||
inheritPolicies = false;
|
inheritPolicies = false;
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
collectionSearchResults: Observable<any[]> = observableOf([]);
|
originalCollection: Collection;
|
||||||
|
|
||||||
selectedCollectionName: string;
|
selectedCollectionName: string;
|
||||||
selectedCollection: Collection;
|
selectedCollection: Collection;
|
||||||
canSubmit = false;
|
canSubmit = false;
|
||||||
@@ -46,23 +43,26 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
item: Item;
|
item: Item;
|
||||||
processing = false;
|
processing = false;
|
||||||
|
|
||||||
pagination = new PaginationComponentOptions();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item's page
|
* Route to the item's page
|
||||||
*/
|
*/
|
||||||
itemPageRoute$: Observable<string>;
|
itemPageRoute$: Observable<string>;
|
||||||
|
|
||||||
|
COLLECTIONS = [DSpaceObjectType.COLLECTION];
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private itemDataService: ItemDataService,
|
private itemDataService: ItemDataService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private translateService: TranslateService) {
|
private translateService: TranslateService,
|
||||||
}
|
private requestService: RequestService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.dso), getFirstSucceededRemoteData()
|
||||||
|
) as Observable<RemoteData<Item>>;
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item))
|
map((item) => getItemPageRoute(item))
|
||||||
@@ -71,43 +71,22 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.pagination.pageSize = 5;
|
this.itemRD$.pipe(
|
||||||
this.loadSuggestions('');
|
getFirstSucceededRemoteData(),
|
||||||
}
|
getRemoteDataPayload(),
|
||||||
|
switchMap((item) => item.owningCollection),
|
||||||
/**
|
getFirstSucceededRemoteData(),
|
||||||
* Find suggestions based on entered query
|
getRemoteDataPayload(),
|
||||||
* @param query - Search query
|
).subscribe((collection) => {
|
||||||
*/
|
this.originalCollection = collection;
|
||||||
findSuggestions(query): void {
|
|
||||||
this.loadSuggestions(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all available collections to move the item to.
|
|
||||||
* TODO: When the API support it, only fetch collections where user has ADD rights to.
|
|
||||||
*/
|
|
||||||
loadSuggestions(query): void {
|
|
||||||
this.collectionSearchResults = this.searchService.search(new PaginatedSearchOptions({
|
|
||||||
pagination: this.pagination,
|
|
||||||
dsoTypes: [DSpaceObjectType.COLLECTION],
|
|
||||||
query: query
|
|
||||||
})).pipe(
|
|
||||||
first(),
|
|
||||||
map((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
|
|
||||||
return rd.payload.page.map((searchResult) => {
|
|
||||||
return searchResult.indexableObject;
|
|
||||||
});
|
});
|
||||||
}) ,
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the collection name and id based on the selected value
|
* Set the collection name and id based on the selected value
|
||||||
* @param data - obtained from the ds-input-suggestions component
|
* @param data - obtained from the ds-input-suggestions component
|
||||||
*/
|
*/
|
||||||
onClick(data: any): void {
|
selectDso(data: any): void {
|
||||||
this.selectedCollection = data;
|
this.selectedCollection = data;
|
||||||
this.selectedCollectionName = data.name;
|
this.selectedCollectionName = data.name;
|
||||||
this.canSubmit = true;
|
this.canSubmit = true;
|
||||||
@@ -123,26 +102,41 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Moves the item to a new collection based on the selected collection
|
* Moves the item to a new collection based on the selected collection
|
||||||
*/
|
*/
|
||||||
moveCollection() {
|
moveToCollection() {
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
|
const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection)
|
||||||
(response: RemoteData<Collection>) => {
|
.pipe(getFirstCompletedRemoteData());
|
||||||
this.router.navigate([getItemEditRoute(this.item)]);
|
|
||||||
|
move$.subscribe((response: RemoteData<any>) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
this.notificationsService.error(this.translateService.get('item.edit.move.error'));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
move$.pipe(
|
||||||
|
switchMap(() => this.requestService.setStaleByHrefSubstring(this.item.id)),
|
||||||
|
switchMap(() =>
|
||||||
|
this.itemDataService.findById(
|
||||||
|
this.item.id,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('owningCollection')
|
||||||
|
)),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe(() => {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
}
|
this.router.navigate([getItemEditRoute(this.item)]);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
discard(): void {
|
||||||
* Resets the can submit when the user changes the content of the input field
|
this.selectedCollection = null;
|
||||||
* @param data
|
|
||||||
*/
|
|
||||||
resetCollection(data: any) {
|
|
||||||
this.canSubmit = false;
|
this.canSubmit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canMove(): boolean {
|
||||||
|
return this.canSubmit && this.selectedCollection?.id !== this.originalCollection.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<ng-container *ngVar="updates$ | async as updates">
|
<ng-container *ngVar="updates$ | async as updates">
|
||||||
<ng-container *ngIf="updates">
|
<ng-container *ngIf="updates && !(loading$ | async)">
|
||||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
|
<ds-pagination
|
||||||
|
[paginationOptions]="paginationConfig"
|
||||||
|
[pageInfoState]="(relationshipsRd$ | async)?.payload?.pageInfo"
|
||||||
|
[collectionSize]="(relationshipsRd$ | async)?.payload?.totalElements + (this.nbAddedFields$ | async)"
|
||||||
|
[hideGear]="true"
|
||||||
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
<div class="my-2">
|
||||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
class="relationship-row d-block alert"
|
class="relationship-row d-block alert"
|
||||||
[fieldUpdate]="updateValue || {}"
|
[fieldUpdate]="updateValue || {}"
|
||||||
@@ -19,8 +26,10 @@
|
|||||||
'alert-danger': updateValue.changeType === 2
|
'alert-danger': updateValue.changeType === 2
|
||||||
}">
|
}">
|
||||||
</ds-edit-relationship>
|
</ds-edit-relationship>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-loading *ngIf="!updates"></ds-loading>
|
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -16,6 +16,12 @@ import { SharedModule } from '../../../../shared/shared.module';
|
|||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||||
|
import { HostWindowService } from '../../../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
||||||
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
let comp: EditRelationshipListComponent;
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
@@ -25,6 +31,8 @@ let linkService;
|
|||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let relationshipService;
|
let relationshipService;
|
||||||
let selectableListService;
|
let selectableListService;
|
||||||
|
let paginationService;
|
||||||
|
let hostWindowService;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
@@ -37,9 +45,21 @@ let fieldUpdate1;
|
|||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
let relationships;
|
let relationships;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
let paginationOptions;
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
|
||||||
|
const resetComponent = () => {
|
||||||
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
comp.item = item;
|
||||||
|
comp.itemType = entityType;
|
||||||
|
comp.url = url;
|
||||||
|
comp.relationshipType = relationshipType;
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
|
||||||
entityType = Object.assign(new ItemType(), {
|
entityType = Object.assign(new ItemType(), {
|
||||||
@@ -63,6 +83,12 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: `er${relationshipType.id}`,
|
||||||
|
pageSize: 5,
|
||||||
|
currentPage: 1,
|
||||||
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: 'author1',
|
||||||
uuid: 'author1'
|
uuid: 'author1'
|
||||||
@@ -141,6 +167,10 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
resolveLinks: () => null,
|
resolveLinks: () => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
paginationService = new PaginationServiceStub(paginationOptions);
|
||||||
|
|
||||||
|
hostWindowService = new HostWindowServiceStub(1200);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedModule, TranslateModule.forRoot()],
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
declarations: [EditRelationshipListComponent],
|
declarations: [EditRelationshipListComponent],
|
||||||
@@ -149,22 +179,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
{ provide: RelationshipService, useValue: relationshipService },
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
{ provide: SelectableListService, useValue: selectableListService },
|
{ provide: SelectableListService, useValue: selectableListService },
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
|
{ provide: HostWindowService, useValue: hostWindowService },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
resetComponent();
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
}));
|
||||||
comp = fixture.componentInstance;
|
|
||||||
de = fixture.debugElement;
|
|
||||||
comp.item = item;
|
|
||||||
comp.itemType = entityType;
|
|
||||||
comp.url = url;
|
|
||||||
comp.relationshipType = relationshipType;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -176,4 +199,82 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
expect(element.classList).toContain('alert-danger');
|
expect(element.classList).toContain('alert-danger');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pagination component', () => {
|
||||||
|
let paginationComp: PaginationComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
paginationComp = de.query(By.css('ds-pagination')).componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive the correct pagination config', () => {
|
||||||
|
expect(paginationComp.paginationOptions).toEqual(paginationOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should receive correct collection size', () => {
|
||||||
|
expect(paginationComp.collectionSize).toEqual(relationships.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('relationshipService.getItemRelationshipsByLabel', () => {
|
||||||
|
it('should receive the correct pagination info', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const findListOptions = callArgs[2];
|
||||||
|
|
||||||
|
expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize);
|
||||||
|
expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the left side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(entityType), // publication
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
||||||
|
leftwardType: 'isAuthorOfPublication',
|
||||||
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
});
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the publication is on the right side of the relationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(entityType), // publication
|
||||||
|
leftwardType: 'isPublicationOfAuthor',
|
||||||
|
rightwardType: 'isAuthorOfPublication',
|
||||||
|
});
|
||||||
|
relationshipService.getItemRelationshipsByLabel.calls.reset();
|
||||||
|
resetComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch isAuthorOfPublication', () => {
|
||||||
|
expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args;
|
||||||
|
const label = callArgs[1];
|
||||||
|
|
||||||
|
expect(label).toEqual('isAuthorOfPublication');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.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 { combineLatest as observableCombineLatest, Observable, of } from 'rxjs';
|
import {
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
from as observableFrom
|
||||||
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
FieldUpdate,
|
FieldUpdate,
|
||||||
FieldUpdates,
|
FieldUpdates,
|
||||||
@@ -11,14 +16,24 @@ import {
|
|||||||
} from '../../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { defaultIfEmpty, map, mergeMap, switchMap, take, startWith } from 'rxjs/operators';
|
import {
|
||||||
import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
|
defaultIfEmpty,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
startWith,
|
||||||
|
toArray,
|
||||||
|
tap
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
import { hasValue, hasValueOperator, hasNoValue } from '../../../../shared/empty.util';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteData,
|
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getAllSucceededRemoteData,
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
@@ -30,6 +45,10 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
|||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { Collection } from '../../../../core/shared/collection.model';
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -40,7 +59,7 @@ import { Collection } from '../../../../core/shared/collection.model';
|
|||||||
* A component creating a list of editable relationships of a certain type
|
* A component creating a list of editable relationships of a certain type
|
||||||
* The relationships are rendered as a list of related items
|
* The relationships are rendered as a list of related items
|
||||||
*/
|
*/
|
||||||
export class EditRelationshipListComponent implements OnInit {
|
export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item to display related items for
|
* The item to display related items for
|
||||||
@@ -60,6 +79,17 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() relationshipType: RelationshipType;
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits the left and right item type of {@link relationshipType} simultaneously.
|
||||||
|
*/
|
||||||
|
private relationshipLeftAndRightType$: Observable<[ItemType, ItemType]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType},
|
||||||
|
* false if it is on the right-hand side and undefined in the rare case that it is on neither side.
|
||||||
|
*/
|
||||||
|
private currentItemIsLeftItem$: Observable<boolean>;
|
||||||
|
|
||||||
private relatedEntityType$: Observable<ItemType>;
|
private relatedEntityType$: Observable<ItemType>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +100,38 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* The FieldUpdates for the relationships in question
|
* The FieldUpdates for the relationships in question
|
||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: BehaviorSubject<FieldUpdates> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RemoteData for the relationships
|
||||||
|
*/
|
||||||
|
relationshipsRd$: BehaviorSubject<RemoteData<PaginatedList<Relationship>>> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the current page is the last page
|
||||||
|
*/
|
||||||
|
isLastPage$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we're loading
|
||||||
|
*/
|
||||||
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of added fields that haven't been saved yet
|
||||||
|
*/
|
||||||
|
nbAddedFields$: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination config
|
||||||
|
*/
|
||||||
|
paginationConfig: PaginationComponentOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to the lookup window
|
* A reference to the lookup window
|
||||||
@@ -82,6 +143,7 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected relationshipService: RelationshipService,
|
protected relationshipService: RelationshipService,
|
||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
protected selectableListService: SelectableListService,
|
protected selectableListService: SelectableListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -172,6 +234,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading$.next(true);
|
||||||
|
// emit the last page again to trigger a fieldupdates refresh
|
||||||
|
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -186,6 +252,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.loading$.next(true);
|
||||||
|
// emit the last page again to trigger a fieldupdates refresh
|
||||||
|
this.relationshipsRd$.next(this.relationshipsRd$.getValue());
|
||||||
};
|
};
|
||||||
this.relatedEntityType$
|
this.relatedEntityType$
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
@@ -212,10 +282,10 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
if (field.relationship) {
|
if (field.relationship) {
|
||||||
return this.getRelatedItem(field.relationship);
|
return this.getRelatedItem(field.relationship);
|
||||||
} else {
|
} else {
|
||||||
return of(field.relatedItem);
|
return observableOf(field.relatedItem);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
) : of([])
|
) : observableOf([])
|
||||||
),
|
),
|
||||||
take(1),
|
take(1),
|
||||||
map((items) => items.map((item) => {
|
map((items) => items.map((item) => {
|
||||||
@@ -267,15 +337,16 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// store the left and right type of the relationship in a single observable
|
||||||
this.relatedEntityType$ =
|
this.relationshipLeftAndRightType$ = observableCombineLatest([
|
||||||
observableCombineLatest([
|
|
||||||
this.relationshipType.leftType,
|
this.relationshipType.leftType,
|
||||||
this.relationshipType.rightType,
|
this.relationshipType.rightType,
|
||||||
].map((type) => type.pipe(
|
].map((type) => type.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
))).pipe(
|
))) as Observable<[ItemType, ItemType]>;
|
||||||
|
|
||||||
|
this.relatedEntityType$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
||||||
hasValueOperator()
|
hasValueOperator()
|
||||||
);
|
);
|
||||||
@@ -286,65 +357,142 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updates$ = this.getItemRelationships().pipe(
|
this.currentItemIsLeftItem$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
switchMap((relationships) =>
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
observableCombineLatest(
|
if (leftType.id === this.itemType.id) {
|
||||||
relationships.map((relationship) => this.relationshipService.isLeftItem(relationship, this.item))
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightType.id === this.itemType.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should never happen...
|
||||||
|
console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize the pagination options
|
||||||
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
|
this.paginationConfig.id = `er${this.relationshipType.id}`;
|
||||||
|
this.paginationConfig.pageSize = 5;
|
||||||
|
this.paginationConfig.currentPage = 1;
|
||||||
|
|
||||||
|
// get the pagination params from the route
|
||||||
|
const currentPagination$ = this.paginationService.getCurrentPagination(
|
||||||
|
this.paginationConfig.id,
|
||||||
|
this.paginationConfig
|
||||||
).pipe(
|
).pipe(
|
||||||
defaultIfEmpty([]),
|
tap(() => this.loading$.next(true))
|
||||||
map((isLeftItemArray) => isLeftItemArray.map((isLeftItem, index) => {
|
);
|
||||||
const relationship = relationships[index];
|
|
||||||
const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
currentPagination$,
|
||||||
|
this.currentItemIsLeftItem$,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) =>
|
||||||
|
// get the relationships for the current item, relationshiptype and page
|
||||||
|
this.relationshipService.getItemRelationshipsByLabel(
|
||||||
|
this.item,
|
||||||
|
currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType,
|
||||||
|
{
|
||||||
|
elementsPerPage: currentPagination.pageSize,
|
||||||
|
currentPage: currentPagination.currentPage,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
followLink('leftItem'),
|
||||||
|
followLink('rightItem'),
|
||||||
|
)),
|
||||||
|
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
|
this.relationshipsRd$.next(rd);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// keep isLastPage$ up to date based on relationshipsRd$
|
||||||
|
this.subs.push(this.relationshipsRd$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
getAllSucceededRemoteData()
|
||||||
|
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
|
this.isLastPage$.next(hasNoValue(rd.payload._links.next));
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.subs.push(this.relationshipsRd$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
getAllSucceededRemoteData(),
|
||||||
|
switchMap((rd: RemoteData<PaginatedList<Relationship>>) =>
|
||||||
|
// emit each relationship in the page separately
|
||||||
|
observableFrom(rd.payload.page).pipe(
|
||||||
|
mergeMap((relationship: Relationship) =>
|
||||||
|
// check for each relationship whether it's the left item
|
||||||
|
this.relationshipService.isLeftItem(relationship, this.item).pipe(
|
||||||
|
// emit an array containing both the relationship and whether it's the left item,
|
||||||
|
// as we'll need both
|
||||||
|
map((isLeftItem: boolean) => [relationship, isLeftItem])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
map(([relationship, isLeftItem]: [Relationship, boolean]) => {
|
||||||
|
// turn it into a RelationshipIdentifiable, an
|
||||||
|
const nameVariant =
|
||||||
|
isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
||||||
return {
|
return {
|
||||||
uuid: relationship.id,
|
uuid: relationship.id,
|
||||||
type: this.relationshipType,
|
type: this.relationshipType,
|
||||||
relationship,
|
relationship,
|
||||||
nameVariant,
|
nameVariant,
|
||||||
} as RelationshipIdentifiable;
|
} as RelationshipIdentifiable;
|
||||||
})),
|
}),
|
||||||
|
// wait until all relationships have been processed, and emit them all as a single array
|
||||||
|
toArray(),
|
||||||
|
// if the pipe above completes without emitting anything, emit an empty array instead
|
||||||
|
defaultIfEmpty([])
|
||||||
)),
|
)),
|
||||||
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields).pipe(
|
switchMap((nextFields: RelationshipIdentifiable[]) => {
|
||||||
map((fieldUpdates) => {
|
// Get a list that contains the unsaved changes for the page, as well as the page of
|
||||||
|
// RelationshipIdentifiables, as a single list of FieldUpdates
|
||||||
|
return this.objectUpdatesService.getFieldUpdates(this.url, nextFields).pipe(
|
||||||
|
map((fieldUpdates: FieldUpdates) => {
|
||||||
const fieldUpdatesFiltered: FieldUpdates = {};
|
const fieldUpdatesFiltered: FieldUpdates = {};
|
||||||
|
this.nbAddedFields$.next(0);
|
||||||
|
// iterate over the fieldupdates and filter out the ones that pertain to this
|
||||||
|
// relationshiptype
|
||||||
Object.keys(fieldUpdates).forEach((uuid) => {
|
Object.keys(fieldUpdates).forEach((uuid) => {
|
||||||
if (hasValue(fieldUpdates[uuid])) {
|
if (hasValue(fieldUpdates[uuid])) {
|
||||||
const field = fieldUpdates[uuid].field;
|
const field = fieldUpdates[uuid].field as RelationshipIdentifiable;
|
||||||
if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) {
|
// only include fieldupdates regarding this RelationshipType
|
||||||
|
if (field.type.id === this.relationshipType.id) {
|
||||||
|
// if it's a newly added relationship
|
||||||
|
if (fieldUpdates[uuid].changeType === FieldChangeType.ADD) {
|
||||||
|
// increase the counter that tracks new relationships
|
||||||
|
this.nbAddedFields$.next(this.nbAddedFields$.getValue() + 1);
|
||||||
|
if (this.isLastPage$.getValue() === true) {
|
||||||
|
// only include newly added relationships to the output if we're on the last
|
||||||
|
// page
|
||||||
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// include all others
|
||||||
|
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return fieldUpdatesFiltered;
|
return fieldUpdatesFiltered;
|
||||||
}),
|
}),
|
||||||
)),
|
|
||||||
startWith({}),
|
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
startWith({}),
|
||||||
|
).subscribe((updates: FieldUpdates) => {
|
||||||
|
this.loading$.next(false);
|
||||||
|
this.updates$.next(updates);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getItemRelationships() {
|
ngOnDestroy(): void {
|
||||||
this.linkService.resolveLink(this.item,
|
this.subs
|
||||||
followLink('relationships', undefined, true, true, true,
|
.filter((subscription) => hasValue(subscription))
|
||||||
followLink('relationshipType'),
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
followLink('leftItem'),
|
|
||||||
followLink('rightItem'),
|
|
||||||
));
|
|
||||||
return this.item.relationships.pipe(
|
|
||||||
getAllSucceededRemoteData(),
|
|
||||||
map((relationships: RemoteData<PaginatedList<Relationship>>) => relationships.payload.page.filter((relationship: Relationship) => hasValue(relationship))),
|
|
||||||
switchMap((itemRelationships: Relationship[]) =>
|
|
||||||
observableCombineLatest(
|
|
||||||
itemRelationships
|
|
||||||
.map((relationship) => relationship.relationshipType.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
))
|
|
||||||
).pipe(
|
|
||||||
defaultIfEmpty([]),
|
|
||||||
map((relationshipTypes) => itemRelationships.filter(
|
|
||||||
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -227,7 +227,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
console.log('init');
|
|
||||||
return this.relationshipService.getRelatedItems(this.item).pipe(
|
return this.relationshipService.getRelatedItems(this.item).pipe(
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe((items: Item[]) => {
|
).subscribe((items: Item[]) => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="simple-view-element" [class.d-none]="content.textContent.trim().length === 0 && hasNoValue(content.querySelector('img'))">
|
<div class="simple-view-element" [class.d-none]="hideIfNoTextContent && content.textContent.trim().length === 0">
|
||||||
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
|
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
|
||||||
<div #content class="simple-view-element-body">
|
<div #content class="simple-view-element-body">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
||||||
@@ -6,35 +6,34 @@ import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.componen
|
|||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-component-without-content',
|
selector: 'ds-component-without-content',
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
|
||||||
'</ds-metadata-field-wrapper>'
|
'</ds-metadata-field-wrapper>'
|
||||||
})
|
})
|
||||||
class NoContentComponent {}
|
class NoContentComponent {
|
||||||
|
public hideIfNoTextContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-component-with-empty-spans',
|
selector: 'ds-component-with-empty-spans',
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
|
||||||
' <span></span>\n' +
|
' <span></span>\n' +
|
||||||
' <span></span>\n' +
|
' <span></span>\n' +
|
||||||
'</ds-metadata-field-wrapper>'
|
'</ds-metadata-field-wrapper>'
|
||||||
})
|
})
|
||||||
class SpanContentComponent {}
|
class SpanContentComponent {
|
||||||
|
@Input() hideIfNoTextContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-component-with-text',
|
selector: 'ds-component-with-text',
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
|
||||||
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
|
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
|
||||||
'</ds-metadata-field-wrapper>'
|
'</ds-metadata-field-wrapper>'
|
||||||
})
|
})
|
||||||
class TextContentComponent {}
|
class TextContentComponent {
|
||||||
|
@Input() hideIfNoTextContent = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-component-with-image',
|
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
|
||||||
' <img src="https://some/image.png" alt="an alt text">\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
|
||||||
})
|
|
||||||
class ImgContentComponent {}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
describe('MetadataFieldWrapperComponent', () => {
|
describe('MetadataFieldWrapperComponent', () => {
|
||||||
@@ -43,7 +42,7 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
|
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -58,6 +57,7 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
expect(component).toBeDefined();
|
expect(component).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with hideIfNoTextContent=true', () => {
|
||||||
it('should not show the component when there is no content', () => {
|
it('should not show the component when there is no content', () => {
|
||||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
@@ -66,7 +66,7 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show the component when there is DOM content but not text or an image', () => {
|
it('should not show the component when there is no text content', () => {
|
||||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
const parentNative = parentFixture.nativeElement;
|
const parentNative = parentFixture.nativeElement;
|
||||||
@@ -82,14 +82,35 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should show the component when there is img content', () => {
|
describe('with hideIfNoTextContent=false', () => {
|
||||||
const parentFixture = TestBed.createComponent(ImgContentComponent);
|
it('should show the component when there is no content', () => {
|
||||||
|
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||||
|
parentFixture.componentInstance.hideIfNoTextContent = false;
|
||||||
|
parentFixture.detectChanges();
|
||||||
|
const parentNative = parentFixture.nativeElement;
|
||||||
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
|
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the component when there is no text content', () => {
|
||||||
|
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||||
|
parentFixture.componentInstance.hideIfNoTextContent = false;
|
||||||
|
parentFixture.detectChanges();
|
||||||
|
const parentNative = parentFixture.nativeElement;
|
||||||
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
|
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the component when there is text content', () => {
|
||||||
|
const parentFixture = TestBed.createComponent(TextContentComponent);
|
||||||
|
parentFixture.componentInstance.hideIfNoTextContent = false;
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
const parentNative = parentFixture.nativeElement;
|
const parentNative = parentFixture.nativeElement;
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { hasNoValue } from '../../../shared/empty.util';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders any content inside this wrapper.
|
* This component renders any content inside this wrapper.
|
||||||
@@ -17,10 +16,5 @@ export class MetadataFieldWrapperComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
/**
|
@Input() hideIfNoTextContent = true;
|
||||||
* Make hasNoValue() available in the template
|
|
||||||
*/
|
|
||||||
hasNoValue(o: any): boolean {
|
|
||||||
return hasNoValue(o);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
[retainScrollPosition]="true">
|
[retainScrollPosition]="true">
|
||||||
|
|
||||||
|
|
||||||
<div class="file-section row" *ngFor="let file of originals?.page;">
|
<div class="file-section row mb-3" *ngFor="let file of originals?.page;">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -68,7 +68,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
followLink('format')
|
followLink('format'),
|
||||||
|
followLink('thumbnail'),
|
||||||
)),
|
)),
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(rd.errorMessage)) {
|
if (hasValue(rd.errorMessage)) {
|
||||||
@@ -85,7 +86,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
{elementsPerPage: options.pageSize, currentPage: options.currentPage},
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
followLink('format')
|
followLink('format'),
|
||||||
|
followLink('thumbnail'),
|
||||||
)),
|
)),
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
if (hasValue(rd.errorMessage)) {
|
if (hasValue(rd.errorMessage)) {
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="simple-view-link my-3">
|
<div class="simple-view-link my-3" *ngIf="!fromWfi">
|
||||||
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
|
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
|
||||||
{{"item.page.link.simple" | translate}}
|
{{"item.page.link.simple" | translate}}
|
||||||
</a>
|
</a>
|
||||||
@@ -29,6 +29,11 @@
|
|||||||
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
|
||||||
|
<div class="button-row bottom" *ngIf="fromWfi">
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="btn btn-outline-secondary mr-1" (click)="back()"><i class="fas fa-arrow-left"></i> {{'item.page.return' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
@@ -29,9 +29,7 @@ const mockItem: Item = Object.assign(new Item(), {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const routeStub = Object.assign(new ActivatedRouteStub(), {
|
|
||||||
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
|
|
||||||
});
|
|
||||||
const metadataServiceStub = {
|
const metadataServiceStub = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
processRemoteData: () => {
|
processRemoteData: () => {
|
||||||
@@ -44,6 +42,10 @@ describe('FullItemPageComponent', () => {
|
|||||||
let fixture: ComponentFixture<FullItemPageComponent>;
|
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||||
|
|
||||||
let authService: AuthService;
|
let authService: AuthService;
|
||||||
|
let routeStub: ActivatedRouteStub;
|
||||||
|
let routeData;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
@@ -51,6 +53,14 @@ describe('FullItemPageComponent', () => {
|
|||||||
setRedirectUrl: {}
|
setRedirectUrl: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
routeData = {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem),
|
||||||
|
};
|
||||||
|
|
||||||
|
routeStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
data: observableOf(routeData)
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
@@ -84,4 +94,21 @@ describe('FullItemPageComponent', () => {
|
|||||||
expect(table.nativeElement.innerHTML).toContain(metadatum.value);
|
expect(table.nativeElement.innerHTML).toContain(metadatum.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show simple view button when not originated from workflow item', () => {
|
||||||
|
expect(comp.fromWfi).toBe(false);
|
||||||
|
const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link'));
|
||||||
|
expect(simpleViewBtn).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show simple view button when originated from workflow', fakeAsync(() => {
|
||||||
|
routeData.wfi = createSuccessfulRemoteDataObject$({ id: 'wfiId'});
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(comp.fromWfi).toBe(true);
|
||||||
|
const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link'));
|
||||||
|
expect(simpleViewBtn).toBeFalsy();
|
||||||
|
});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {filter, map} from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Data, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable , BehaviorSubject } from 'rxjs';
|
import { Observable , BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ import { ItemDataService } from '../../core/data/item-data.service';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a full item page.
|
* This component renders a full item page.
|
||||||
@@ -29,14 +29,25 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
export class FullItemPageComponent extends ItemPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
itemRD$: BehaviorSubject<RemoteData<Item>>;
|
itemRD$: BehaviorSubject<RemoteData<Item>>;
|
||||||
|
|
||||||
metadata$: Observable<MetadataMap>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) {
|
/**
|
||||||
super(route, router, items, metadataService, authService);
|
* True when the itemRD has been originated from its workflowitem, false otherwise.
|
||||||
|
*/
|
||||||
|
fromWfi = false;
|
||||||
|
|
||||||
|
subs = [];
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
router: Router,
|
||||||
|
items: ItemDataService,
|
||||||
|
authService: AuthService,
|
||||||
|
private _location: Location) {
|
||||||
|
super(route, router, items, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||||
@@ -46,5 +57,21 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
map((rd: RemoteData<Item>) => rd.payload),
|
map((rd: RemoteData<Item>) => rd.payload),
|
||||||
filter((item: Item) => hasValue(item)),
|
filter((item: Item) => hasValue(item)),
|
||||||
map((item: Item) => item.metadata),);
|
map((item: Item) => item.metadata),);
|
||||||
|
|
||||||
|
this.subs.push(this.route.data.subscribe((data: Data) => {
|
||||||
|
this.fromWfi = hasValue(data.wfi);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate back in browser history.
|
||||||
|
*/
|
||||||
|
back() {
|
||||||
|
this._location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,39 +4,24 @@ import { Observable } from 'rxjs';
|
|||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { getItemPageRoute } from './item-page-routing-paths';
|
import { getItemPageRoute } from './item-page-routing-paths';
|
||||||
|
import { ItemResolver } from './item.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The self links defined in this list are expected to be requested somewhere in the near future
|
* This class represents a resolver that requests a specific item before the route is activated and will redirect to the
|
||||||
* Requesting them as embeds will limit the number of requests
|
* entity page
|
||||||
*/
|
|
||||||
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
|
||||||
followLink('owningCollection', undefined, true, true, true,
|
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
|
||||||
followLink('parentCommunity'))
|
|
||||||
),
|
|
||||||
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
|
|
||||||
followLink('relationships'),
|
|
||||||
followLink('version', undefined, true, true, true, followLink('versionhistory')),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class represents a resolver that requests a specific item before the route is activated
|
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
export class ItemPageResolver extends ItemResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private itemService: ItemDataService,
|
protected itemService: ItemDataService,
|
||||||
private store: Store<any>,
|
protected store: Store<any>,
|
||||||
private router: Router
|
protected router: Router
|
||||||
) {
|
) {
|
||||||
|
super(itemService, store, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,12 +32,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
* or an error if something went wrong
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
const itemRD$ = this.itemService.findById(route.params.id,
|
return super.resolve(route, state).pipe(
|
||||||
true,
|
|
||||||
false,
|
|
||||||
...ITEM_PAGE_LINKS_TO_FOLLOW
|
|
||||||
).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((rd: RemoteData<Item>) => {
|
map((rd: RemoteData<Item>) => {
|
||||||
if (rd.hasSucceeded && hasValue(rd.payload)) {
|
if (rd.hasSucceeded && hasValue(rd.payload)) {
|
||||||
const itemRoute = getItemPageRoute(rd.payload);
|
const itemRoute = getItemPageRoute(rd.payload);
|
||||||
@@ -66,11 +46,5 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
return rd;
|
return rd;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
|
|
||||||
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload));
|
|
||||||
});
|
|
||||||
|
|
||||||
return itemRD$;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
src/app/+item-page/item.resolver.ts
Normal file
60
src/app/+item-page/item.resolver.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
||||||
|
followLink('owningCollection', {},
|
||||||
|
followLink('parentCommunity', {},
|
||||||
|
followLink('parentCommunity'))
|
||||||
|
),
|
||||||
|
followLink('relationships'),
|
||||||
|
followLink('version', {}, followLink('versionhistory')),
|
||||||
|
followLink('thumbnail')
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ItemResolver implements Resolve<RemoteData<Item>> {
|
||||||
|
constructor(
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected store: Store<any>,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving an item based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
|
const itemRD$ = this.itemService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...ITEM_PAGE_LINKS_TO_FOLLOW
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return itemRD$;
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,11 @@ import { ItemPageFieldComponent } from '../item-page-field.component';
|
|||||||
templateUrl: '../item-page-field.component.html'
|
templateUrl: '../item-page-field.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This component is used for displaying the author (dc.contributor.author, dc.creator and dc.contributor) metadata of an item
|
* This component is used for displaying the author (dc.contributor.author, dc.creator and
|
||||||
|
* dc.contributor) metadata of an item.
|
||||||
|
*
|
||||||
|
* Note that it purely deals with metadata. It won't turn related Person authors into links to their
|
||||||
|
* item page. For that use a {@link MetadataRepresentationListComponent} instead.
|
||||||
*/
|
*/
|
||||||
export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {
|
export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
@@ -8,8 +8,6 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
@@ -51,10 +49,9 @@ export class ItemPageComponent implements OnInit {
|
|||||||
itemPageRoute$: Observable<string>;
|
itemPageRoute$: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private items: ItemDataService,
|
private items: ItemDataService,
|
||||||
private metadataService: MetadataService,
|
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ export class ItemPageComponent implements OnInit {
|
|||||||
map((data) => data.dso as RemoteData<Item>),
|
map((data) => data.dso as RemoteData<Item>),
|
||||||
redirectOn4xx(this.router, this.authService)
|
redirectOn4xx(this.router, this.authService)
|
||||||
);
|
);
|
||||||
this.metadataService.processRemoteData(this.itemRD$);
|
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item))
|
map((item) => getItemPageRoute(item))
|
||||||
|
@@ -9,8 +9,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ng-container *ngIf="!mediaViewer.image">
|
<ng-container *ngIf="!mediaViewer.image">
|
||||||
<ds-metadata-field-wrapper>
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="mediaViewer.image">
|
<ng-container *ngIf="mediaViewer.image">
|
||||||
@@ -18,7 +18,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
<ds-metadata-representation-list class="ds-item-page-mixed-author-field"
|
||||||
|
[parentItem]="object"
|
||||||
|
[itemType]="'Person'"
|
||||||
|
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||||
|
[label]="'relationships.isAuthorOf' | translate">
|
||||||
|
</ds-metadata-representation-list>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['journal.title']"
|
[fields]="['journal.title']"
|
||||||
[label]="'publication.page.journal-title'">
|
[label]="'publication.page.journal-title'">
|
||||||
@@ -37,12 +42,6 @@
|
|||||||
</ds-generic-item-page-field>
|
</ds-generic-item-page-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<ds-metadata-representation-list
|
|
||||||
[parentItem]="object"
|
|
||||||
[itemType]="'Person'"
|
|
||||||
[metadataField]="'dc.contributor.author'"
|
|
||||||
[label]="'relationships.isAuthorOf' | translate">
|
|
||||||
</ds-metadata-representation-list>
|
|
||||||
<ds-related-items
|
<ds-related-items
|
||||||
[parentItem]="object"
|
[parentItem]="object"
|
||||||
[relationType]="'isProjectOfPublication'"
|
[relationType]="'isProjectOfPublication'"
|
||||||
|
@@ -88,9 +88,14 @@ describe('PublicationComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a component to display the author', () => {
|
it('should not contain a metadata only author field', () => {
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||||
|
expect(fields.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a component to display the abstract', () => {
|
it('should contain a component to display the abstract', () => {
|
||||||
|
@@ -1,10 +1,6 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
|
||||||
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -21,19 +17,10 @@ export class ItemComponent implements OnInit {
|
|||||||
* Route to the item page
|
* Route to the item page
|
||||||
*/
|
*/
|
||||||
itemPageRoute: string;
|
itemPageRoute: string;
|
||||||
mediaViewer = environment.mediaViewer;
|
|
||||||
|
|
||||||
constructor(protected bitstreamDataService: BitstreamDataService) {
|
mediaViewer = environment.mediaViewer;
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemPageRoute = getItemPageRoute(this.object);
|
this.itemPageRoute = getItemPageRoute(this.object);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
|
||||||
getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -9,8 +9,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ng-container *ngIf="!mediaViewer.image">
|
<ng-container *ngIf="!mediaViewer.image">
|
||||||
<ds-metadata-field-wrapper>
|
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
||||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="mediaViewer.image">
|
<ng-container *ngIf="mediaViewer.image">
|
||||||
@@ -18,7 +18,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
<ds-metadata-representation-list class="ds-item-page-mixed-author-field"
|
||||||
|
[parentItem]="object"
|
||||||
|
[itemType]="'Person'"
|
||||||
|
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
||||||
|
[label]="'relationships.isAuthorOf' | translate">
|
||||||
|
</ds-metadata-representation-list>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['journal.title']"
|
[fields]="['journal.title']"
|
||||||
[label]="'item.page.journal-title'">
|
[label]="'item.page.journal-title'">
|
||||||
@@ -37,12 +42,6 @@
|
|||||||
</ds-generic-item-page-field>
|
</ds-generic-item-page-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<ds-metadata-representation-list
|
|
||||||
[parentItem]="object"
|
|
||||||
[itemType]="'Person'"
|
|
||||||
[metadataField]="'dc.contributor.author'"
|
|
||||||
[label]="'relationships.isAuthorOf' | translate">
|
|
||||||
</ds-metadata-representation-list>
|
|
||||||
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
||||||
<ds-generic-item-page-field [item]="object"
|
<ds-generic-item-page-field [item]="object"
|
||||||
[fields]="['dc.description']"
|
[fields]="['dc.description']"
|
||||||
|
@@ -89,9 +89,14 @@ describe('UntypedItemComponent', () => {
|
|||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a component to display the author', () => {
|
it('should not contain a metadata only author field', () => {
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
expect(fields.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a mixed metadata and relationship field for authors', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
|
||||||
|
expect(fields.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain a component to display the abstract', () => {
|
it('should contain a component to display the abstract', () => {
|
||||||
|
@@ -5,13 +5,13 @@ import { MetadataRepresentationListComponent } from './metadata-representation-l
|
|||||||
import { RelationshipService } from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
const itemType = 'Person';
|
const itemType = 'Person';
|
||||||
const metadataField = 'dc.contributor.author';
|
const metadataFields = ['dc.contributor.author', 'dc.creator'];
|
||||||
const parentItem: Item = Object.assign(new Item(), {
|
const parentItem: Item = Object.assign(new Item(), {
|
||||||
id: 'parent-item',
|
id: 'parent-item',
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -28,6 +28,20 @@ const parentItem: Item = Object.assign(new Item(), {
|
|||||||
place: 1
|
place: 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
'dc.creator': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Related Creator with authority',
|
||||||
|
authority: 'virtual::related-creator',
|
||||||
|
place: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Related Creator with authority - unauthorized',
|
||||||
|
authority: 'virtual::related-creator-unauthorized',
|
||||||
|
place: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
'dc.title': [
|
'dc.title': [
|
||||||
{
|
{
|
||||||
language: null,
|
language: null,
|
||||||
@@ -47,21 +61,49 @@ const relatedAuthor: Item = Object.assign(new Item(), {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const relation: Relationship = Object.assign(new Relationship(), {
|
const relatedCreator: Item = Object.assign(new Item(), {
|
||||||
|
id: 'related-creator',
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'Related Creator'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dspace.entity.type': 'Person',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const authorRelation: Relationship = Object.assign(new Relationship(), {
|
||||||
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||||
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
|
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
|
||||||
});
|
});
|
||||||
let relationshipService: RelationshipService;
|
const creatorRelation: Relationship = Object.assign(new Relationship(), {
|
||||||
|
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||||
|
rightItem: createSuccessfulRemoteDataObject$(relatedCreator),
|
||||||
|
});
|
||||||
|
const creatorRelationUnauthorized: Relationship = Object.assign(new Relationship(), {
|
||||||
|
leftItem: createSuccessfulRemoteDataObject$(parentItem),
|
||||||
|
rightItem: createFailedRemoteDataObject$('Unauthorized', 401),
|
||||||
|
});
|
||||||
|
let relationshipService;
|
||||||
|
|
||||||
describe('MetadataRepresentationListComponent', () => {
|
describe('MetadataRepresentationListComponent', () => {
|
||||||
let comp: MetadataRepresentationListComponent;
|
let comp: MetadataRepresentationListComponent;
|
||||||
let fixture: ComponentFixture<MetadataRepresentationListComponent>;
|
let fixture: ComponentFixture<MetadataRepresentationListComponent>;
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
relationshipService = {
|
||||||
{
|
findById: (id: string) => {
|
||||||
findById: createSuccessfulRemoteDataObject$(relation)
|
if (id === 'related-author') {
|
||||||
|
return createSuccessfulRemoteDataObject$(authorRelation);
|
||||||
}
|
}
|
||||||
);
|
if (id === 'related-creator') {
|
||||||
|
return createSuccessfulRemoteDataObject$(creatorRelation);
|
||||||
|
}
|
||||||
|
if (id === 'related-creator-unauthorized') {
|
||||||
|
return createSuccessfulRemoteDataObject$(creatorRelationUnauthorized);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -81,13 +123,13 @@ describe('MetadataRepresentationListComponent', () => {
|
|||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.parentItem = parentItem;
|
comp.parentItem = parentItem;
|
||||||
comp.itemType = itemType;
|
comp.itemType = itemType;
|
||||||
comp.metadataField = metadataField;
|
comp.metadataFields = metadataFields;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should load 2 ds-metadata-representation-loader components', () => {
|
it('should load 4 ds-metadata-representation-loader components', () => {
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader'));
|
const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader'));
|
||||||
expect(fields.length).toBe(2);
|
expect(fields.length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain one page of items', () => {
|
it('should contain one page of items', () => {
|
||||||
|
@@ -42,7 +42,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
|||||||
/**
|
/**
|
||||||
* The metadata field to use for fetching metadata from the item
|
* The metadata field to use for fetching metadata from the item
|
||||||
*/
|
*/
|
||||||
@Input() metadataField: string;
|
@Input() metadataFields: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An i18n label to use as a title for the list
|
* An i18n label to use as a title for the list
|
||||||
@@ -70,7 +70,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
|||||||
* @param page The page to fetch
|
* @param page The page to fetch
|
||||||
*/
|
*/
|
||||||
getPage(page: number): Observable<MetadataRepresentation[]> {
|
getPage(page: number): Observable<MetadataRepresentation[]> {
|
||||||
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
|
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataFields);
|
||||||
this.total = metadata.length;
|
this.total = metadata.length;
|
||||||
return this.resolveMetadataRepresentations(metadata, page);
|
return this.resolveMetadataRepresentations(metadata, page);
|
||||||
}
|
}
|
||||||
@@ -91,9 +91,11 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
|
|||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
switchMap((relRD: RemoteData<Relationship>) =>
|
switchMap((relRD: RemoteData<Relationship>) =>
|
||||||
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
|
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
|
||||||
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded),
|
filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted),
|
||||||
map(([leftItem, rightItem]) => {
|
map(([leftItem, rightItem]) => {
|
||||||
if (leftItem.payload.id === this.parentItem.id) {
|
if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) {
|
||||||
|
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
|
||||||
|
} else if (rightItem.hasSucceeded && leftItem.payload.id === this.parentItem.id) {
|
||||||
return rightItem.payload;
|
return rightItem.payload;
|
||||||
} else if (rightItem.payload.id === this.parentItem.id) {
|
} else if (rightItem.payload.id === this.parentItem.id) {
|
||||||
return leftItem.payload;
|
return leftItem.payload;
|
||||||
|
@@ -16,9 +16,11 @@ import { SearchResult } from '../shared/search/search-result.model';
|
|||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { currentPath } from '../shared/utils/route.utils';
|
import { currentPath } from '../shared/utils/route.utils';
|
||||||
import { Router} from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Context } from '../core/shared/context.model';
|
import { Context } from '../core/shared/context.model';
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search',
|
selector: 'ds-search',
|
||||||
@@ -128,8 +130,11 @@ export class SearchComponent implements OnInit {
|
|||||||
this.searchLink = this.getSearchLink();
|
this.searchLink = this.getSearchLink();
|
||||||
this.searchOptions$ = this.getSearchOptions();
|
this.searchOptions$ = this.getSearchOptions();
|
||||||
this.sub = this.searchOptions$.pipe(
|
this.sub = this.searchOptions$.pipe(
|
||||||
switchMap((options) => this.service.search(options).pipe(getFirstSucceededRemoteData(), startWith(undefined))))
|
switchMap((options) => this.service.search(
|
||||||
.subscribe((results) => {
|
options, undefined, true, true, followLink<Item>('thumbnail', { isOptional: true })
|
||||||
|
).pipe(getFirstSucceededRemoteData(), startWith(undefined))
|
||||||
|
)
|
||||||
|
).subscribe((results) => {
|
||||||
this.resultsRD$.next(results);
|
this.resultsRD$.next(results);
|
||||||
});
|
});
|
||||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { ItemFromWorkflowResolver } from './item-from-workflow.resolver';
|
||||||
|
|
||||||
|
describe('ItemFromWorkflowResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: ItemFromWorkflowResolver;
|
||||||
|
let wfiService: WorkflowItemDataService;
|
||||||
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
const itemUuid = '8888-8888-8888-8888';
|
||||||
|
const wfi = {
|
||||||
|
id: uuid,
|
||||||
|
item: createSuccessfulRemoteDataObject$({ id: itemUuid })
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wfiService = {
|
||||||
|
findById: (id: string) => createSuccessfulRemoteDataObject$(wfi)
|
||||||
|
} as any;
|
||||||
|
resolver = new ItemFromWorkflowResolver(wfiService, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a an item from from the workflow item with the correct id', (done) => {
|
||||||
|
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(resolved) => {
|
||||||
|
expect(resolved.payload.id).toEqual(itemUuid);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { followLink } from '../shared/utils/follow-link-config.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkflowItem } from '../core/submission/models/workflowitem.model';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ItemFromWorkflowResolver implements Resolve<RemoteData<Item>> {
|
||||||
|
constructor(
|
||||||
|
private workflowItemService: WorkflowItemDataService,
|
||||||
|
protected store: Store<any>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving an item based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
|
const itemRD$ = this.workflowItemService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
followLink('item'),
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((wfiRD: RemoteData<WorkflowItem>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
return itemRD$;
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,9 @@ export function getWorkflowItemPageRoute(wfiId: string) {
|
|||||||
export function getWorkflowItemEditRoute(wfiId: string) {
|
export function getWorkflowItemEditRoute(wfiId: string) {
|
||||||
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString();
|
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString();
|
||||||
}
|
}
|
||||||
|
export function getWorkflowItemViewRoute(wfiId: string) {
|
||||||
|
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_VIEW_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export function getWorkflowItemDeleteRoute(wfiId: string) {
|
export function getWorkflowItemDeleteRoute(wfiId: string) {
|
||||||
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString();
|
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString();
|
||||||
@@ -19,4 +22,5 @@ export function getWorkflowItemSendBackRoute(wfiId: string) {
|
|||||||
|
|
||||||
export const WORKFLOW_ITEM_EDIT_PATH = 'edit';
|
export const WORKFLOW_ITEM_EDIT_PATH = 'edit';
|
||||||
export const WORKFLOW_ITEM_DELETE_PATH = 'delete';
|
export const WORKFLOW_ITEM_DELETE_PATH = 'delete';
|
||||||
|
export const WORKFLOW_ITEM_VIEW_PATH = 'view';
|
||||||
export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback';
|
export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback';
|
||||||
|
@@ -6,12 +6,15 @@ import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
|
|||||||
import {
|
import {
|
||||||
WORKFLOW_ITEM_DELETE_PATH,
|
WORKFLOW_ITEM_DELETE_PATH,
|
||||||
WORKFLOW_ITEM_EDIT_PATH,
|
WORKFLOW_ITEM_EDIT_PATH,
|
||||||
WORKFLOW_ITEM_SEND_BACK_PATH
|
WORKFLOW_ITEM_SEND_BACK_PATH,
|
||||||
|
WORKFLOW_ITEM_VIEW_PATH
|
||||||
} from './workflowitems-edit-page-routing-paths';
|
} from './workflowitems-edit-page-routing-paths';
|
||||||
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
||||||
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
||||||
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { ItemFromWorkflowResolver } from './item-from-workflow.resolver';
|
||||||
|
import { ThemedFullItemPageComponent } from '../+item-page/full/themed-full-item-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -29,6 +32,16 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
},
|
},
|
||||||
data: { title: 'workflow-item.edit.title', breadcrumbKey: 'workflow-item.edit' }
|
data: { title: 'workflow-item.edit.title', breadcrumbKey: 'workflow-item.edit' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
path: WORKFLOW_ITEM_VIEW_PATH,
|
||||||
|
component: ThemedFullItemPageComponent,
|
||||||
|
resolve: {
|
||||||
|
dso: ItemFromWorkflowResolver,
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'workflow-item.view.title', breadcrumbKey: 'workflow-item.view' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
canActivate: [AuthenticatedGuard],
|
canActivate: [AuthenticatedGuard],
|
||||||
path: WORKFLOW_ITEM_DELETE_PATH,
|
path: WORKFLOW_ITEM_DELETE_PATH,
|
||||||
@@ -51,7 +64,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
}]
|
}]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
providers: [WorkflowItemPageResolver]
|
providers: [WorkflowItemPageResolver, ItemFromWorkflowResolver]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This module defines the default component to load when navigating to the workflowitems edit page path.
|
* This module defines the default component to load when navigating to the workflowitems edit page path.
|
||||||
|
@@ -7,6 +7,8 @@ import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-ite
|
|||||||
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
|
||||||
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
||||||
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
||||||
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
|
import { ItemPageModule } from '../+item-page/item-page.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -14,8 +16,15 @@ import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/t
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
SubmissionModule,
|
SubmissionModule,
|
||||||
|
StatisticsModule,
|
||||||
|
ItemPageModule
|
||||||
],
|
],
|
||||||
declarations: [WorkflowItemDeleteComponent, ThemedWorkflowItemDeleteComponent, WorkflowItemSendBackComponent, ThemedWorkflowItemSendBackComponent]
|
declarations: [
|
||||||
|
WorkflowItemDeleteComponent,
|
||||||
|
ThemedWorkflowItemDeleteComponent,
|
||||||
|
WorkflowItemSendBackComponent,
|
||||||
|
ThemedWorkflowItemSendBackComponent
|
||||||
|
]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This module handles all modules that need to access the workflowitems edit page.
|
* This module handles all modules that need to access the workflowitems edit page.
|
||||||
|
@@ -3,6 +3,10 @@ import { getAccessControlModuleRoute } from '../app-routing-paths';
|
|||||||
|
|
||||||
export const GROUP_EDIT_PATH = 'groups';
|
export const GROUP_EDIT_PATH = 'groups';
|
||||||
|
|
||||||
|
export function getGroupsRoute() {
|
||||||
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export function getGroupEditRoute(id: string) {
|
export function getGroupEditRoute(id: string) {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo
|
|||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { GroupPageGuard } from './group-registry/group-page.guard';
|
||||||
|
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
|
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,7 +18,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }
|
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
||||||
|
canActivate: [SiteAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: GROUP_EDIT_PATH,
|
path: GROUP_EDIT_PATH,
|
||||||
@@ -23,7 +27,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }
|
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||||
@@ -31,7 +36,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }
|
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||||
@@ -39,7 +45,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }
|
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
|
||||||
|
canActivate: [GroupPageGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
@@ -0,0 +1,83 @@
|
|||||||
|
import { GroupPageGuard } from './group-page.guard';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
describe('GroupPageGuard', () => {
|
||||||
|
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
|
||||||
|
const groupUuid = '0d6f89df-f95a-4829-943c-f21f434fb892';
|
||||||
|
const groupEndpointUrl = `${groupsEndpointUrl}/${groupUuid}`;
|
||||||
|
const routeSnapshotWithGroupId = {
|
||||||
|
params: {
|
||||||
|
groupId: groupUuid,
|
||||||
|
}
|
||||||
|
} as unknown as ActivatedRouteSnapshot;
|
||||||
|
|
||||||
|
let guard: GroupPageGuard;
|
||||||
|
let halEndpointService: HALEndpointService;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
|
||||||
|
(halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj(['isAuthorized']);
|
||||||
|
// NOTE: value is set in beforeEach
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj(['parseUrl']);
|
||||||
|
(router as any).parseUrl.and.returnValue = {};
|
||||||
|
|
||||||
|
authService = jasmine.createSpyObj(['isAuthenticated']);
|
||||||
|
(authService as any).isAuthenticated.and.returnValue(observableOf(true));
|
||||||
|
|
||||||
|
guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(guard).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
describe('when the current user can manage the group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(
|
||||||
|
routeSnapshotWithGroupId, { url: 'current-url'} as any
|
||||||
|
).subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined
|
||||||
|
);
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the current user can not manage the group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return true', (done) => {
|
||||||
|
guard.canActivate(
|
||||||
|
routeSnapshotWithGroupId, { url: 'current-url'} as any
|
||||||
|
).subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined
|
||||||
|
);
|
||||||
|
expect(result).not.toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
35
src/app/access-control/group-registry/group-page.guard.ts
Normal file
35
src/app/access-control/group-registry/group-page.guard.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
|
||||||
|
|
||||||
|
protected groupsEndpoint = 'groups';
|
||||||
|
|
||||||
|
constructor(protected halEndpointService: HALEndpointService,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return observableOf([FeatureID.CanManageGroup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
|
||||||
|
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -33,9 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfoState$"
|
[pageInfoState]="pageInfoState$"
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
@@ -59,11 +59,23 @@
|
|||||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
<ng-container [ngSwitch]="groupDto.ableToEdit">
|
||||||
class="btn btn-outline-primary btn-sm"
|
<button *ngSwitchCase="true"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}">
|
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||||
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
|
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}"
|
||||||
|
>
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button *ngSwitchCase="false"
|
||||||
|
[disabled]="true"
|
||||||
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
|
placement="left"
|
||||||
|
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||||
|
@@ -30,6 +30,7 @@ import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
|||||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
describe('GroupRegistryComponent', () => {
|
describe('GroupRegistryComponent', () => {
|
||||||
let component: GroupsRegistryComponent;
|
let component: GroupsRegistryComponent;
|
||||||
@@ -43,6 +44,26 @@ describe('GroupRegistryComponent', () => {
|
|||||||
let mockEPeople;
|
let mockEPeople;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set authorizationService.isAuthorized to return the following values.
|
||||||
|
* @param isAdmin whether or not the current user is an admin.
|
||||||
|
* @param canManageGroup whether or not the current user can manage all groups.
|
||||||
|
*/
|
||||||
|
const setIsAuthorized = (isAdmin: boolean, canManageGroup: boolean) => {
|
||||||
|
(authorizationService as any).isAuthorized.and.callFake((featureId?: FeatureID) => {
|
||||||
|
switch (featureId) {
|
||||||
|
case FeatureID.AdministratorOf:
|
||||||
|
return observableOf(isAdmin);
|
||||||
|
case FeatureID.CanManageGroup:
|
||||||
|
return observableOf(canManageGroup);
|
||||||
|
case FeatureID.CanDelete:
|
||||||
|
return observableOf(true);
|
||||||
|
default:
|
||||||
|
throw new Error(`setIsAuthorized: this fake implementation does not support ${featureId}.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
mockGroups = [GroupMock, GroupMock2];
|
mockGroups = [GroupMock, GroupMock2];
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
@@ -131,9 +152,8 @@ describe('GroupRegistryComponent', () => {
|
|||||||
return createSuccessfulRemoteDataObject$(undefined);
|
return createSuccessfulRemoteDataObject$(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
|
||||||
isAuthorized: observableOf(true)
|
setIsAuthorized(true, true);
|
||||||
});
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
@@ -180,6 +200,81 @@ describe('GroupRegistryComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('edit buttons', () => {
|
||||||
|
describe('when the user is a general admin', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
// NOTE: setting canManageGroup to false should not matter, since isAdmin takes priority
|
||||||
|
setIsAuthorized(true, false);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not check the canManageGroup permissions', () => {
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[0].self
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[0].self, undefined // treated differently
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[1].self
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[1].self, undefined // treated differently
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user can edit the groups', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
setIsAuthorized(false, true);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user can not edit the groups', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
setIsAuthorized(false, false);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching with query', () => {
|
describe('when searching with query', () => {
|
||||||
let groupIdsFound;
|
let groupIdsFound;
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
@@ -75,7 +75,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A boolean representing if a search is pending
|
* A boolean representing if a search is pending
|
||||||
*/
|
*/
|
||||||
searching$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
// Current search in groups registry
|
// Current search in groups registry
|
||||||
currentSearchQuery: string;
|
currentSearchQuery: string;
|
||||||
@@ -118,12 +118,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param data Contains query param
|
* @param data Contains query param
|
||||||
*/
|
*/
|
||||||
search(data: any) {
|
search(data: any) {
|
||||||
this.searching$.next(true);
|
|
||||||
if (hasValue(this.searchSub)) {
|
if (hasValue(this.searchSub)) {
|
||||||
this.searchSub.unsubscribe();
|
this.searchSub.unsubscribe();
|
||||||
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
||||||
}
|
}
|
||||||
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
|
tap(() => this.loading$.next(true)),
|
||||||
switchMap((paginationOptions) => {
|
switchMap((paginationOptions) => {
|
||||||
const query: string = data.query;
|
const query: string = data.query;
|
||||||
if (query != null && this.currentSearchQuery !== query) {
|
if (query != null && this.currentSearchQuery !== query) {
|
||||||
@@ -141,18 +141,22 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
if (groups.page.length === 0) {
|
if (groups.page.length === 0) {
|
||||||
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||||
}
|
}
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
|
switchMap((isSiteAdmin: boolean) => {
|
||||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||||
if (!this.deletedGroupsIds.includes(group.id)) {
|
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||||
return observableCombineLatest([
|
return observableCombineLatest([
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||||
|
this.canManageGroup$(isSiteAdmin, group),
|
||||||
this.hasLinkedDSO(group),
|
this.hasLinkedDSO(group),
|
||||||
this.getSubgroups(group),
|
this.getSubgroups(group),
|
||||||
this.getMembers(group)
|
this.getMembers(group)
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
|
map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
|
||||||
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
[boolean, boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
||||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||||
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
|
||||||
|
groupDtoModel.ableToEdit = canManageGroup;
|
||||||
groupDtoModel.group = group;
|
groupDtoModel.group = group;
|
||||||
groupDtoModel.subgroups = subgroups.payload;
|
groupDtoModel.subgroups = subgroups.payload;
|
||||||
groupDtoModel.epersons = members.payload;
|
groupDtoModel.epersons = members.payload;
|
||||||
@@ -165,15 +169,25 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
return buildPaginatedList(groups.pageInfo, dtos);
|
return buildPaginatedList(groups.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||||
this.groupsDto$.next(value);
|
this.groupsDto$.next(value);
|
||||||
this.pageInfoState$.next(value.pageInfo);
|
this.pageInfoState$.next(value.pageInfo);
|
||||||
this.searching$.next(false);
|
this.loading$.next(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subs.push(this.searchSub);
|
this.subs.push(this.searchSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canManageGroup$(isSiteAdmin: boolean, group: Group): Observable<boolean> {
|
||||||
|
if (isSiteAdmin) {
|
||||||
|
return observableOf(true);
|
||||||
|
} else {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanManageGroup, group.self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete Group
|
* Delete Group
|
||||||
*/
|
*/
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
<ds-themed-root [isNotAuthBlocking]="isNotAuthBlocking$ | async" [isLoading]="isLoading$ | async"></ds-themed-root>
|
<ds-themed-root
|
||||||
|
[shouldShowFullscreenLoader]="(isAuthBlocking$ | async) || (isThemeLoading$ | async)"
|
||||||
|
[shouldShowRouteLoader]="isRouteLoading$ | async"></ds-themed-root>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
import { delay, distinctUntilChanged, filter, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
Optional,
|
Optional,
|
||||||
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -32,11 +33,13 @@ import { LocaleService } from './core/locale/locale.service';
|
|||||||
import { hasValue, isNotEmpty } from './shared/empty.util';
|
import { hasValue, isNotEmpty } from './shared/empty.util';
|
||||||
import { KlaroService } from './shared/cookies/klaro.service';
|
import { KlaroService } from './shared/cookies/klaro.service';
|
||||||
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
import { ThemeService } from './shared/theme-support/theme.service';
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
||||||
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
||||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -45,7 +48,6 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, AfterViewInit {
|
export class AppComponent implements OnInit, AfterViewInit {
|
||||||
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
|
||||||
sidebarVisible: Observable<boolean>;
|
sidebarVisible: Observable<boolean>;
|
||||||
slideSidebarOver: Observable<boolean>;
|
slideSidebarOver: Observable<boolean>;
|
||||||
collapsedSidebarWidth: Observable<string>;
|
collapsedSidebarWidth: Observable<string>;
|
||||||
@@ -57,11 +59,28 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
/**
|
/**
|
||||||
* Whether or not the authentication is currently blocking the UI
|
* Whether or not the authentication is currently blocking the UI
|
||||||
*/
|
*/
|
||||||
isNotAuthBlocking$: Observable<boolean>;
|
isAuthBlocking$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the app is in the process of rerouting
|
||||||
|
*/
|
||||||
|
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the theme is in the process of being swapped
|
||||||
|
*/
|
||||||
|
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the idle modal is is currently open
|
||||||
|
*/
|
||||||
|
idleModalOpen: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
@Inject(DOCUMENT) private document: any,
|
@Inject(DOCUMENT) private document: any,
|
||||||
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
private themeService: ThemeService,
|
private themeService: ThemeService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private store: Store<HostWindowState>,
|
private store: Store<HostWindowState>,
|
||||||
@@ -75,6 +94,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService,
|
private localeService: LocaleService,
|
||||||
private breadcrumbsService: BreadcrumbsService,
|
private breadcrumbsService: BreadcrumbsService,
|
||||||
|
private modalService: NgbModal,
|
||||||
@Optional() private cookiesService: KlaroService,
|
@Optional() private cookiesService: KlaroService,
|
||||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||||
) {
|
) {
|
||||||
@@ -83,6 +103,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.models = models;
|
this.models = models;
|
||||||
|
|
||||||
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
// the theme css will never download server side, so this should only happen on the browser
|
||||||
|
this.isThemeLoading$.next(true);
|
||||||
|
}
|
||||||
if (hasValue(themeName)) {
|
if (hasValue(themeName)) {
|
||||||
this.setThemeCss(themeName);
|
this.setThemeCss(themeName);
|
||||||
} else if (hasValue(DEFAULT_THEME_CONFIG)) {
|
} else if (hasValue(DEFAULT_THEME_CONFIG)) {
|
||||||
@@ -92,6 +116,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
this.authService.trackTokenExpiration();
|
||||||
|
this.trackIdleModal();
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
|
|
||||||
@@ -114,17 +143,15 @@ 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(
|
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||||
map((isBlocking: boolean) => isBlocking === false),
|
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
this.isNotAuthBlocking$
|
this.isAuthBlocking$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((notBlocking: boolean) => notBlocking),
|
filter((isBlocking: boolean) => isBlocking === false),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe(() => this.initializeKlaro());
|
).subscribe(() => this.initializeKlaro());
|
||||||
|
|
||||||
@@ -156,12 +183,12 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
delay(0)
|
delay(0)
|
||||||
).subscribe((event) => {
|
).subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
this.isLoading$.next(true);
|
this.isRouteLoading$.next(true);
|
||||||
} else if (
|
} else if (
|
||||||
event instanceof NavigationEnd ||
|
event instanceof NavigationEnd ||
|
||||||
event instanceof NavigationCancel
|
event instanceof NavigationCancel
|
||||||
) {
|
) {
|
||||||
this.isLoading$.next(false);
|
this.isRouteLoading$.next(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -209,7 +236,28 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// the fact that this callback is used, proves we're on the browser.
|
||||||
|
this.isThemeLoading$.next(false);
|
||||||
};
|
};
|
||||||
head.appendChild(link);
|
head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trackIdleModal() {
|
||||||
|
const isIdle$ = this.authService.isUserIdle();
|
||||||
|
const isAuthenticated$ = this.authService.isAuthenticated();
|
||||||
|
isIdle$.pipe(withLatestFrom(isAuthenticated$))
|
||||||
|
.subscribe(([userIdle, authenticated]) => {
|
||||||
|
if (userIdle && authenticated) {
|
||||||
|
if (!this.idleModalOpen) {
|
||||||
|
const modalRef = this.modalService.open(IdleModalComponent, { ariaLabelledBy: 'idle-modal.header' });
|
||||||
|
this.idleModalOpen = true;
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((closed: boolean) => {
|
||||||
|
if (closed) {
|
||||||
|
this.idleModalOpen = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,6 +46,8 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component
|
|||||||
import { ThemedHeaderComponent } from './header/themed-header.component';
|
import { ThemedHeaderComponent } from './header/themed-header.component';
|
||||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||||
|
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -129,6 +131,7 @@ const DECLARATIONS = [
|
|||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
ThemedHeaderComponent,
|
ThemedHeaderComponent,
|
||||||
HeaderNavbarWrapperComponent,
|
HeaderNavbarWrapperComponent,
|
||||||
|
ThemedHeaderNavbarWrapperComponent,
|
||||||
AdminSidebarComponent,
|
AdminSidebarComponent,
|
||||||
AdminSidebarSectionComponent,
|
AdminSidebarSectionComponent,
|
||||||
ExpandableAdminSidebarSectionComponent,
|
ExpandableAdminSidebarSectionComponent,
|
||||||
@@ -142,6 +145,7 @@ const DECLARATIONS = [
|
|||||||
ThemedBreadcrumbsComponent,
|
ThemedBreadcrumbsComponent,
|
||||||
ForbiddenComponent,
|
ForbiddenComponent,
|
||||||
ThemedForbiddenComponent,
|
ThemedForbiddenComponent,
|
||||||
|
IdleModalComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -174,8 +174,8 @@ export class CommunityListService {
|
|||||||
direction: options.sort.direction
|
direction: options.sort.direction
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
followLink('subcommunities', this.configOnePage, true, true),
|
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
||||||
followLink('collections', this.configOnePage, true, true))
|
followLink('collections', { findListOptions: this.configOnePage }))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
map((results) => results.payload),
|
map((results) => results.payload),
|
||||||
@@ -242,8 +242,8 @@ export class CommunityListService {
|
|||||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||||
currentPage: i
|
currentPage: i
|
||||||
},
|
},
|
||||||
followLink('subcommunities', this.configOnePage, true, true),
|
followLink('subcommunities', { findListOptions: this.configOnePage }),
|
||||||
followLink('collections', this.configOnePage, true, true))
|
followLink('collections', { findListOptions: this.configOnePage }))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((rd: RemoteData<PaginatedList<Community>>) => {
|
switchMap((rd: RemoteData<PaginatedList<Community>>) => {
|
||||||
|
@@ -34,7 +34,9 @@ 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')
|
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'),
|
||||||
|
SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'),
|
||||||
|
UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -292,10 +294,13 @@ export class ResetAuthenticationMessagesAction implements Action {
|
|||||||
export class RetrieveAuthMethodsAction implements Action {
|
export class RetrieveAuthMethodsAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS;
|
||||||
|
|
||||||
payload: AuthStatus;
|
payload: {
|
||||||
|
status: AuthStatus;
|
||||||
|
blocking: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(authStatus: AuthStatus) {
|
constructor(status: AuthStatus, blocking: boolean) {
|
||||||
this.payload = authStatus;
|
this.payload = { status, blocking };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,10 +311,14 @@ export class RetrieveAuthMethodsAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class RetrieveAuthMethodsSuccessAction implements Action {
|
export class RetrieveAuthMethodsSuccessAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_SUCCESS;
|
||||||
payload: AuthMethod[];
|
|
||||||
|
|
||||||
constructor(authMethods: AuthMethod[] ) {
|
payload: {
|
||||||
this.payload = authMethods;
|
authMethods: AuthMethod[];
|
||||||
|
blocking: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(authMethods: AuthMethod[], blocking: boolean ) {
|
||||||
|
this.payload = { authMethods, blocking };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +329,12 @@ export class RetrieveAuthMethodsSuccessAction implements Action {
|
|||||||
*/
|
*/
|
||||||
export class RetrieveAuthMethodsErrorAction implements Action {
|
export class RetrieveAuthMethodsErrorAction implements Action {
|
||||||
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
public type: string = AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR;
|
||||||
|
|
||||||
|
payload: boolean;
|
||||||
|
|
||||||
|
constructor(blocking: boolean) {
|
||||||
|
this.payload = blocking;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -391,6 +406,24 @@ export class RetrieveAuthenticatedEpersonErrorAction implements Action {
|
|||||||
this.payload = payload ;
|
this.payload = payload ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current user as being idle.
|
||||||
|
* @class SetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class SetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.SET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset the current user as being idle.
|
||||||
|
* @class UnsetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class UnsetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.UNSET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -421,4 +454,7 @@ export type AuthActions
|
|||||||
| RetrieveAuthenticatedEpersonErrorAction
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
| RetrieveAuthenticatedEpersonSuccessAction
|
| RetrieveAuthenticatedEpersonSuccessAction
|
||||||
| SetRedirectUrlAction
|
| SetRedirectUrlAction
|
||||||
| RedirectAfterLoginSuccessAction;
|
| RedirectAfterLoginSuccessAction
|
||||||
|
| SetUserAsIdleAction
|
||||||
|
| UnsetUserAsIdleAction;
|
||||||
|
|
||||||
|
@@ -43,10 +43,12 @@ describe('AuthEffects', () => {
|
|||||||
let initialState;
|
let initialState;
|
||||||
let token;
|
let token;
|
||||||
let store: MockStore<AppState>;
|
let store: MockStore<AppState>;
|
||||||
|
let authStatus;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
authServiceStub = new AuthServiceStub();
|
authServiceStub = new AuthServiceStub();
|
||||||
token = authServiceStub.getToken();
|
token = authServiceStub.getToken();
|
||||||
|
authStatus = Object.assign(new AuthStatus(), {});
|
||||||
initialState = {
|
initialState = {
|
||||||
core: {
|
core: {
|
||||||
auth: {
|
auth: {
|
||||||
@@ -217,19 +219,41 @@ describe('AuthEffects', () => {
|
|||||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on CSR', () => {
|
||||||
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||||
observableOf(
|
observableOf(
|
||||||
{ authenticated: false })
|
{ authenticated: false })
|
||||||
);
|
);
|
||||||
|
spyOn((authEffects as any).authService, 'getRetrieveAuthMethodsAction').and.returnValue(
|
||||||
|
new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, false)
|
||||||
|
);
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus) });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, false) });
|
||||||
|
|
||||||
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on SSR', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(
|
||||||
|
observableOf(
|
||||||
|
{ authenticated: false })
|
||||||
|
);
|
||||||
|
spyOn((authEffects as any).authService, 'getRetrieveAuthMethodsAction').and.returnValue(
|
||||||
|
new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, true)
|
||||||
|
);
|
||||||
|
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsAction({ authenticated: false } as AuthStatus, true) });
|
||||||
|
|
||||||
|
expect(authEffects.checkTokenCookie$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when check token failed', () => {
|
describe('when check token failed', () => {
|
||||||
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
it('should return a AUTHENTICATED_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action', () => {
|
||||||
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue(observableThrow(new Error('Message Error test')));
|
||||||
@@ -359,11 +383,17 @@ describe('AuthEffects', () => {
|
|||||||
|
|
||||||
describe('retrieveMethods$', () => {
|
describe('retrieveMethods$', () => {
|
||||||
|
|
||||||
|
describe('on CSR', () => {
|
||||||
describe('when retrieve authentication methods succeeded', () => {
|
describe('when retrieve authentication methods succeeded', () => {
|
||||||
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: false}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock) });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock, false) });
|
||||||
|
|
||||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
@@ -373,15 +403,56 @@ describe('AuthEffects', () => {
|
|||||||
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTH_METHODS } });
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: false}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction() });
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction(false) });
|
||||||
|
|
||||||
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on SSR', () => {
|
||||||
|
describe('when retrieve authentication methods succeeded', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS_SUCCESS action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: true}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsSuccessAction(authMethodsMock, true) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when retrieve authentication methods failed', () => {
|
||||||
|
it('should return a RETRIEVE_AUTH_METHODS_ERROR action in response to a RETRIEVE_AUTH_METHODS action', () => {
|
||||||
|
spyOn((authEffects as any).authService, 'retrieveAuthMethodsFromAuthStatus').and.returnValue(observableThrow(''));
|
||||||
|
|
||||||
|
actions = hot('--a-', { a:
|
||||||
|
{
|
||||||
|
type: AuthActionTypes.RETRIEVE_AUTH_METHODS,
|
||||||
|
payload: { status: authStatus, blocking: true}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new RetrieveAuthMethodsErrorAction(true) });
|
||||||
|
|
||||||
|
expect(authEffects.retrieveMethods$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('clearInvalidTokenOnRehydrate$', () => {
|
describe('clearInvalidTokenOnRehydrate$', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
|
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import {
|
||||||
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
timer,
|
||||||
|
asyncScheduler, queueScheduler
|
||||||
|
} from 'rxjs';
|
||||||
|
import { catchError, filter, map, switchMap, take, tap, observeOn } from 'rxjs/operators';
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import { Action, select, Store } from '@ngrx/store';
|
import { Action, select, Store } from '@ngrx/store';
|
||||||
@@ -37,9 +43,19 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
RetrieveTokenAction
|
RetrieveTokenAction, SetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { RequestActionTypes } from '../data/request.actions';
|
||||||
|
import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions';
|
||||||
|
import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler';
|
||||||
|
import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler';
|
||||||
|
|
||||||
|
// Action Types that do not break/prevent the user from an idle state
|
||||||
|
const IDLE_TIMER_IGNORE_TYPES: string[]
|
||||||
|
= [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE),
|
||||||
|
...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthEffects {
|
export class AuthEffects {
|
||||||
@@ -145,7 +161,7 @@ export class AuthEffects {
|
|||||||
if (response.authenticated) {
|
if (response.authenticated) {
|
||||||
return new RetrieveTokenAction();
|
return new RetrieveTokenAction();
|
||||||
} else {
|
} else {
|
||||||
return new RetrieveAuthMethodsAction(response);
|
return this.authService.getRetrieveAuthMethodsAction(response);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
catchError((error) => observableOf(new AuthenticatedErrorAction(error)))
|
||||||
@@ -234,21 +250,43 @@ export class AuthEffects {
|
|||||||
.pipe(
|
.pipe(
|
||||||
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
ofType(AuthActionTypes.RETRIEVE_AUTH_METHODS),
|
||||||
switchMap((action: RetrieveAuthMethodsAction) => {
|
switchMap((action: RetrieveAuthMethodsAction) => {
|
||||||
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload)
|
return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload.status)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)),
|
map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels, action.payload.blocking)),
|
||||||
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction()))
|
catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction(action.payload.blocking)))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer
|
||||||
|
* If the idleness timer runs out (so no un-ignored action come through for that amount of time)
|
||||||
|
* => Return the action to set the user as idle ({@link SetUserAsIdleAction})
|
||||||
|
* @method trackIdleness
|
||||||
|
*/
|
||||||
|
@Effect()
|
||||||
|
public trackIdleness$: Observable<Action> = this.actions$.pipe(
|
||||||
|
filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)),
|
||||||
|
// Using switchMap the effect will stop subscribing to the previous timer if a new action comes
|
||||||
|
// in, and start a new timer
|
||||||
|
switchMap(() =>
|
||||||
|
// Start a timer outside of Angular's zone
|
||||||
|
timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler))
|
||||||
|
),
|
||||||
|
// Re-enter the zone to dispatch the action
|
||||||
|
observeOn(new EnterZoneScheduler(this.zone, queueScheduler)),
|
||||||
|
map(() => new SetUserAsIdleAction()),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {Actions} actions$
|
* @param {Actions} actions$
|
||||||
|
* @param {NgZone} zone
|
||||||
* @param {AuthService} authService
|
* @param {AuthService} authService
|
||||||
* @param {Store} store
|
* @param {Store} store
|
||||||
*/
|
*/
|
||||||
constructor(private actions$: Actions,
|
constructor(private actions$: Actions,
|
||||||
|
private zone: NgZone,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private store: Store<AppState>) {
|
private store: Store<AppState>) {
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
|
|
||||||
import { catchError, filter, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { Injectable, Injector } from '@angular/core';
|
import { Injectable, Injector } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
@@ -12,14 +12,13 @@ import {
|
|||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseBase
|
HttpResponseBase
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { find } from 'lodash';
|
|
||||||
|
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util';
|
||||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
import { RedirectWhenTokenExpiredAction } from './auth.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
@@ -28,7 +27,7 @@ import { AuthMethodType } from './models/auth.method-type';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
// Intercetor is called twice per request,
|
// Interceptor is called twice per request,
|
||||||
// so to prevent RefreshTokenAction is dispatched twice
|
// so to prevent RefreshTokenAction is dispatched twice
|
||||||
// we're creating a refresh token request list
|
// we're creating a refresh token request list
|
||||||
protected refreshTokenRequestUrls = [];
|
protected refreshTokenRequestUrls = [];
|
||||||
@@ -216,23 +215,8 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
let authorization: string;
|
let authorization: string;
|
||||||
|
|
||||||
if (authService.isTokenExpired()) {
|
if (authService.isTokenExpired()) {
|
||||||
authService.setRedirectUrl(this.router.url);
|
|
||||||
// The access token is expired
|
|
||||||
// Redirect to the login route
|
|
||||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
|
||||||
return observableOf(null);
|
return observableOf(null);
|
||||||
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
||||||
// Intercept a request that is not to the authentication endpoint
|
|
||||||
authService.isTokenExpiring().pipe(
|
|
||||||
filter((isExpiring) => isExpiring))
|
|
||||||
.subscribe(() => {
|
|
||||||
// If the current request url is already in the refresh token request list, skip it
|
|
||||||
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
|
|
||||||
// When a token is about to expire, refresh it
|
|
||||||
this.store.dispatch(new RefreshTokenAction(token));
|
|
||||||
this.refreshTokenRequestUrls.push(req.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Get the auth header from the service.
|
// Get the auth header from the service.
|
||||||
authorization = authService.buildAuthHeader(token);
|
authorization = authService.buildAuthHeader(token);
|
||||||
let newHeaders = req.headers.set('authorization', authorization);
|
let newHeaders = req.headers.set('authorization', authorization);
|
||||||
|
@@ -23,7 +23,7 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction, SetUserAsIdleAction, UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||||
@@ -44,6 +44,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticateAction('user', 'password');
|
const action = new AuthenticateAction('user', 'password');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -53,7 +54,8 @@ describe('authReducer', () => {
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -66,7 +68,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -81,7 +84,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationErrorAction(mockError);
|
const action = new AuthenticationErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -92,7 +96,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: 'Test error message'
|
error: 'Test error message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -105,7 +110,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedAction(mockTokenInfo);
|
const action = new AuthenticatedAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -115,7 +121,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -127,7 +134,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -138,7 +146,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -150,7 +159,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedErrorAction(mockError);
|
const action = new AuthenticatedErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -161,7 +171,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -172,6 +183,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenAction();
|
const action = new CheckAuthenticationTokenAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -180,6 +192,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -190,6 +203,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenCookieAction();
|
const action = new CheckAuthenticationTokenCookieAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -198,6 +212,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -211,7 +226,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutAction();
|
const action = new LogOutAction();
|
||||||
@@ -229,7 +245,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutSuccessAction();
|
const action = new LogOutSuccessAction();
|
||||||
@@ -243,7 +260,8 @@ describe('authReducer', () => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -257,7 +275,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutErrorAction(mockError);
|
const action = new LogOutErrorAction(mockError);
|
||||||
@@ -270,7 +289,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -283,7 +303,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -295,7 +316,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -307,7 +329,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -318,7 +341,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -332,7 +356,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenAction(newTokenInfo);
|
const action = new RefreshTokenAction(newTokenInfo);
|
||||||
@@ -346,7 +371,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -361,7 +387,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
||||||
@@ -375,7 +402,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: false
|
refreshing: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -390,7 +418,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RefreshTokenErrorAction();
|
const action = new RefreshTokenErrorAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -403,7 +432,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -417,7 +447,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -428,7 +459,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: 'Message',
|
info: 'Message',
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,6 +482,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AddAuthenticationMessageAction('Message');
|
const action = new AddAuthenticationMessageAction('Message');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -458,7 +491,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -470,7 +504,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new ResetAuthenticationMessagesAction();
|
const action = new ResetAuthenticationMessagesAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -480,7 +515,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -490,7 +526,8 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new SetRedirectUrlAction('redirect.url');
|
const action = new SetRedirectUrlAction('redirect.url');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -499,7 +536,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
redirectUrl: 'redirect.url'
|
redirectUrl: 'redirect.url',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -510,16 +548,18 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthMethodsAction(new AuthStatus());
|
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -530,41 +570,136 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||||
];
|
];
|
||||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
const action = new RetrieveAuthMethodsSuccessAction(authMethods, false);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: authMethods
|
authMethods: authMethods,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action', () => {
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_SUCCESS action with blocking as true', () => {
|
||||||
initialState = {
|
initialState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
const authMethods = [
|
||||||
|
new AuthMethod(AuthMethodType.Password),
|
||||||
|
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
||||||
|
];
|
||||||
|
const action = new RetrieveAuthMethodsSuccessAction(authMethods, true);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: false,
|
||||||
|
authMethods: authMethods,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action ', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new RetrieveAuthMethodsErrorAction();
|
const action = new RetrieveAuthMethodsErrorAction(false);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
state = {
|
state = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a SET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new SetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a UNSET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new UnsetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a RETRIEVE_AUTH_METHODS_ERROR action with blocking as true', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: true,
|
||||||
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new RetrieveAuthMethodsErrorAction(true);
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: false,
|
||||||
|
loaded: false,
|
||||||
|
blocking: true,
|
||||||
|
loading: false,
|
||||||
|
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
@@ -10,6 +10,7 @@ import {
|
|||||||
RedirectWhenTokenExpiredAction,
|
RedirectWhenTokenExpiredAction,
|
||||||
RefreshTokenSuccessAction,
|
RefreshTokenSuccessAction,
|
||||||
RetrieveAuthenticatedEpersonSuccessAction,
|
RetrieveAuthenticatedEpersonSuccessAction,
|
||||||
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
@@ -58,6 +59,9 @@ export interface AuthState {
|
|||||||
// all authentication Methods enabled at the backend
|
// all authentication Methods enabled at the backend
|
||||||
authMethods?: AuthMethod[];
|
authMethods?: AuthMethod[];
|
||||||
|
|
||||||
|
// true when the current user is idle
|
||||||
|
idle: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +72,8 @@ const initialState: AuthState = {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,6 +193,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authToken: (action as RefreshTokenSuccessAction).payload,
|
authToken: (action as RefreshTokenSuccessAction).payload,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
|
blocking: false
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.ADD_MESSAGE:
|
case AuthActionTypes.ADD_MESSAGE:
|
||||||
@@ -211,14 +217,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
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,
|
blocking: (action as RetrieveAuthMethodsSuccessAction).payload.blocking,
|
||||||
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload
|
authMethods: (action as RetrieveAuthMethodsSuccessAction).payload.authMethods
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
blocking: (action as RetrieveAuthMethodsErrorAction).payload,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,6 +239,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.SET_USER_AS_IDLE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.UNSET_USER_AS_IDLE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: false,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
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';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
@@ -47,6 +52,7 @@ describe('AuthService test', () => {
|
|||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
let authenticatedState;
|
let authenticatedState;
|
||||||
let unAuthenticatedState;
|
let unAuthenticatedState;
|
||||||
|
let idleState;
|
||||||
let linkService;
|
let linkService;
|
||||||
let hardRedirectService;
|
let hardRedirectService;
|
||||||
|
|
||||||
@@ -64,14 +70,24 @@ describe('AuthService test', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
user: EPersonMock
|
user: EPersonMock,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
unAuthenticatedState = {
|
unAuthenticatedState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
user: undefined
|
user: undefined,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
idleState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: token,
|
||||||
|
user: EPersonMock,
|
||||||
|
idle: true
|
||||||
};
|
};
|
||||||
authRequest = new AuthRequestServiceStub();
|
authRequest = new AuthRequestServiceStub();
|
||||||
routeStub = new ActivatedRouteStub();
|
routeStub = new ActivatedRouteStub();
|
||||||
@@ -107,6 +123,8 @@ describe('AuthService test', () => {
|
|||||||
{ provide: Store, useValue: mockStore },
|
{ provide: Store, useValue: mockStore },
|
||||||
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
CookieService,
|
CookieService,
|
||||||
AuthService
|
AuthService
|
||||||
],
|
],
|
||||||
@@ -180,6 +198,26 @@ describe('AuthService test', () => {
|
|||||||
expect(authMethods.length).toBe(2);
|
expect(authMethods.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setIdle true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.setIdle(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('store should dispatch SetUserAsIdleAction', () => {
|
||||||
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new SetUserAsIdleAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setIdle false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.setIdle(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('store should dispatch UnsetUserAsIdleAction', () => {
|
||||||
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new UnsetUserAsIdleAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -207,13 +245,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(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, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true when user is logged in', () => {
|
it('should return true when user is logged in', () => {
|
||||||
@@ -250,6 +288,12 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('isUserIdle should return false when user is not yet idle', () => {
|
||||||
|
authService.isUserIdle().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -277,7 +321,7 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
||||||
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
||||||
authenticatedState = {
|
authenticatedState = {
|
||||||
@@ -292,7 +336,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, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
storage = (authService as any).storage;
|
storage = (authService as any).storage;
|
||||||
routeServiceMock = TestBed.inject(RouteService);
|
routeServiceMock = TestBed.inject(RouteService);
|
||||||
routerStub = TestBed.inject(Router);
|
routerStub = TestBed.inject(Router);
|
||||||
@@ -493,13 +537,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(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, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return null for the shortlived token', () => {
|
it('should return null for the shortlived token', () => {
|
||||||
@@ -508,4 +552,44 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when user is idle', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ authReducer }, {
|
||||||
|
runtimeChecks: {
|
||||||
|
strictStateImmutability: false,
|
||||||
|
strictActionImmutability: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthRequestService, useValue: authRequest },
|
||||||
|
{ provide: REQUEST, useValue: {} },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: linkService },
|
||||||
|
CookieService,
|
||||||
|
AuthService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = idleState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('isUserIdle should return true when user is not idle', () => {
|
||||||
|
authService.isUserIdle().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -29,13 +29,17 @@ import {
|
|||||||
getRedirectUrl,
|
getRedirectUrl,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAuthenticatedLoaded,
|
isAuthenticatedLoaded,
|
||||||
|
isIdle,
|
||||||
isTokenRefreshing
|
isTokenRefreshing
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction, RefreshTokenAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction,
|
||||||
SetRedirectUrlAction
|
RetrieveAuthMethodsAction,
|
||||||
|
SetRedirectUrlAction,
|
||||||
|
SetUserAsIdleAction,
|
||||||
|
UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
@@ -45,6 +49,9 @@ import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
|||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
@@ -63,6 +70,11 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
protected _authenticated: boolean;
|
protected _authenticated: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer to track time until token refresh
|
||||||
|
*/
|
||||||
|
private tokenRefreshTimer;
|
||||||
|
|
||||||
constructor(@Inject(REQUEST) protected req: any,
|
constructor(@Inject(REQUEST) protected req: any,
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
@Optional() @Inject(RESPONSE) private response: any,
|
@Optional() @Inject(RESPONSE) private response: any,
|
||||||
@@ -72,7 +84,9 @@ export class AuthService {
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected storage: CookieService,
|
protected storage: CookieService,
|
||||||
protected store: Store<AppState>,
|
protected store: Store<AppState>,
|
||||||
protected hardRedirectService: HardRedirectService
|
protected hardRedirectService: HardRedirectService,
|
||||||
|
private notificationService: NotificationsService,
|
||||||
|
private translateService: TranslateService
|
||||||
) {
|
) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
@@ -186,7 +200,7 @@ export class AuthService {
|
|||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(getAuthenticatedUserId),
|
select(getAuthenticatedUserId),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((id: string) => this.epersonService.findById(id) ),
|
switchMap((id: string) => this.epersonService.findById(id)),
|
||||||
getAllSucceededRemoteDataPayload()
|
getAllSucceededRemoteDataPayload()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -297,7 +311,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
public getToken(): AuthTokenInfo {
|
public getToken(): AuthTokenInfo {
|
||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
this.store.pipe(select(getAuthenticationToken))
|
this.store.pipe(take(1), select(getAuthenticationToken))
|
||||||
.subscribe((authTokenInfo: AuthTokenInfo) => {
|
.subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
// Retrieve authentication token info and check if is valid
|
// Retrieve authentication token info and check if is valid
|
||||||
token = authTokenInfo || null;
|
token = authTokenInfo || null;
|
||||||
@@ -305,6 +319,44 @@ export class AuthService {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that checks when the session token from store expires and refreshes it when needed
|
||||||
|
*/
|
||||||
|
public trackTokenExpiration(): void {
|
||||||
|
let token: AuthTokenInfo;
|
||||||
|
let currentlyRefreshingToken = false;
|
||||||
|
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
|
// If new token is undefined an it wasn't previously => Refresh failed
|
||||||
|
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
|
||||||
|
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
||||||
|
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
||||||
|
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
|
||||||
|
currentlyRefreshingToken = false;
|
||||||
|
}
|
||||||
|
// If new token.expires is different => Refresh succeeded
|
||||||
|
if (currentlyRefreshingToken && authTokenInfo !== undefined && token.expires !== authTokenInfo.expires) {
|
||||||
|
currentlyRefreshingToken = false;
|
||||||
|
}
|
||||||
|
// Check if/when token needs to be refreshed
|
||||||
|
if (!currentlyRefreshingToken) {
|
||||||
|
token = authTokenInfo || null;
|
||||||
|
if (token !== undefined && token !== null) {
|
||||||
|
let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
|
||||||
|
if (timeLeftBeforeRefresh < 0) {
|
||||||
|
timeLeftBeforeRefresh = 0;
|
||||||
|
}
|
||||||
|
if (hasValue(this.tokenRefreshTimer)) {
|
||||||
|
clearTimeout(this.tokenRefreshTimer);
|
||||||
|
}
|
||||||
|
this.tokenRefreshTimer = setTimeout(() => {
|
||||||
|
this.store.dispatch(new RefreshTokenAction(token));
|
||||||
|
currentlyRefreshingToken = true;
|
||||||
|
}, timeLeftBeforeRefresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a token is next to be expired
|
* Check if a token is next to be expired
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -345,7 +397,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
|
|
||||||
// Save cookie with the token
|
// Save cookie with the token
|
||||||
return this.storage.set(TOKENITEM, token, options);
|
return this.storage.set(TOKENITEM, token, options);
|
||||||
@@ -395,12 +447,15 @@ export class AuthService {
|
|||||||
* @param redirectUrl
|
* @param redirectUrl
|
||||||
*/
|
*/
|
||||||
public navigateToRedirectUrl(redirectUrl: string) {
|
public navigateToRedirectUrl(redirectUrl: string) {
|
||||||
|
// Don't do redirect if already on reload url
|
||||||
|
if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) {
|
||||||
let url = `/reload/${new Date().getTime()}`;
|
let url = `/reload/${new Date().getTime()}`;
|
||||||
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
||||||
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
||||||
}
|
}
|
||||||
this.hardRedirectService.redirect(url);
|
this.hardRedirectService.redirect(url);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh route navigated
|
* Refresh route navigated
|
||||||
@@ -434,7 +489,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
this.storage.set(REDIRECT_COOKIE, url, options);
|
this.storage.set(REDIRECT_COOKIE, url, options);
|
||||||
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||||
}
|
}
|
||||||
@@ -518,4 +573,33 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new instance of RetrieveAuthMethodsAction
|
||||||
|
*
|
||||||
|
* @param authStatus The auth status
|
||||||
|
*/
|
||||||
|
getRetrieveAuthMethodsAction(authStatus: AuthStatus): RetrieveAuthMethodsAction {
|
||||||
|
return new RetrieveAuthMethodsAction(authStatus, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if current user is idle
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public isUserIdle(): Observable<boolean> {
|
||||||
|
return this.store.pipe(select(isIdle));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set idle of auth state
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public setIdle(idle: boolean): void {
|
||||||
|
if (idle) {
|
||||||
|
this.store.dispatch(new SetUserAsIdleAction());
|
||||||
|
} else {
|
||||||
|
this.store.dispatch(new UnsetUserAsIdleAction());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -115,6 +115,14 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
|||||||
|
|
||||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle.
|
||||||
|
* @function _isIdle
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isIdle = (state: AuthState) => state.idle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication methods enabled at the backend
|
* Returns the authentication methods enabled at the backend
|
||||||
* @function getAuthenticationMethods
|
* @function getAuthenticationMethods
|
||||||
@@ -231,3 +239,12 @@ export const getRegistrationError = createSelector(getAuthState, _getRegistratio
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle
|
||||||
|
* @function isIdle
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isIdle = createSelector(getAuthState, _isIdle);
|
||||||
|
@@ -10,6 +10,7 @@ import { AuthService } from './auth.service';
|
|||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RetrieveAuthMethodsAction } from './auth.actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth service.
|
* The auth service.
|
||||||
@@ -60,4 +61,13 @@ export class ServerAuthService extends AuthService {
|
|||||||
map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload))
|
map((rd: RemoteData<AuthStatus>) => Object.assign(new AuthStatus(), rd.payload))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new instance of RetrieveAuthMethodsAction
|
||||||
|
*
|
||||||
|
* @param authStatus The auth status
|
||||||
|
*/
|
||||||
|
getRetrieveAuthMethodsAction(authStatus: AuthStatus): RetrieveAuthMethodsAction {
|
||||||
|
return new RetrieveAuthMethodsAction(authStatus, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { ItemDataService } from '../data/item-data.service';
|
|||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver';
|
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for an Item
|
* The class that resolves the BreadcrumbConfig object for an Item
|
||||||
|
@@ -127,7 +127,8 @@ describe('BrowseService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getBrowseEntriesFor and findList', () => {
|
describe('getBrowseEntriesFor and findList', () => {
|
||||||
const mockAuthorName = 'Donald Smith';
|
// should contain special characters such that url encoding can be tested as well
|
||||||
|
const mockAuthorName = 'Donald Smith & Sons';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
requestService = getMockRequestService(getRequestEntry$(true));
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
@@ -152,7 +153,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('when findList is called with a valid browse definition id', () => {
|
describe('when findList is called with a valid browse definition id', () => {
|
||||||
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
|
||||||
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName;
|
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
|
||||||
|
|
||||||
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
@@ -130,7 +130,7 @@ export class BrowseService {
|
|||||||
args.push(`startsWith=${options.startsWith}`);
|
args.push(`startsWith=${options.startsWith}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(filterValue)) {
|
if (isNotEmpty(filterValue)) {
|
||||||
args.push(`filterValue=${filterValue}`);
|
args.push(`filterValue=${encodeURIComponent(filterValue)}`);
|
||||||
}
|
}
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
href = new URLCombiner(href, `?${args.join('&')}`).toString();
|
||||||
|
10
src/app/core/cache/builders/link.service.spec.ts
vendored
10
src/app/core/cache/builders/link.service.spec.ts
vendored
@@ -102,7 +102,7 @@ describe('LinkService', () => {
|
|||||||
describe('resolveLink', () => {
|
describe('resolveLink', () => {
|
||||||
describe(`when the linkdefinition concerns a single object`, () => {
|
describe(`when the linkdefinition concerns a single object`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
it('should call dataservice.findByHref with the correct href and nested links', () => {
|
||||||
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
|
expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor'));
|
||||||
@@ -116,7 +116,7 @@ describe('LinkService', () => {
|
|||||||
propertyName: 'predecessor',
|
propertyName: 'predecessor',
|
||||||
isList: true
|
isList: true
|
||||||
});
|
});
|
||||||
service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor')));
|
||||||
});
|
});
|
||||||
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => {
|
||||||
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
expect(testDataService.findAllByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor'));
|
||||||
@@ -124,7 +124,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
describe('either way', () => {
|
describe('either way', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getLinkDefinition with the correct model and link', () => {
|
it('should call getLinkDefinition with the correct model and link', () => {
|
||||||
@@ -149,7 +149,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe('LinkService', () => {
|
|||||||
});
|
});
|
||||||
it('should throw an error', () => {
|
it('should throw an error', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor')));
|
service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor')));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
7
src/app/core/cache/builders/link.service.ts
vendored
7
src/app/core/cache/builders/link.service.ts
vendored
@@ -55,9 +55,7 @@ export class LinkService {
|
|||||||
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
||||||
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
|
||||||
|
|
||||||
if (hasNoValue(matchingLinkDef)) {
|
if (hasValue(matchingLinkDef)) {
|
||||||
throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
|
||||||
} else {
|
|
||||||
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
const provider = this.getDataServiceFor(matchingLinkDef.resourceType);
|
||||||
|
|
||||||
if (hasNoValue(provider)) {
|
if (hasNoValue(provider)) {
|
||||||
@@ -84,7 +82,10 @@ export class LinkService {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!linkToFollow.isOptional) {
|
||||||
|
throw new Error(`followLink('${linkToFollow.name}') was used as a required link for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -523,7 +523,7 @@ describe('RemoteDataBuildService', () => {
|
|||||||
let paginatedLinksToFollow;
|
let paginatedLinksToFollow;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
paginatedLinksToFollow = [
|
paginatedLinksToFollow = [
|
||||||
followLink('page', undefined, true, true, true, ...linksToFollow),
|
followLink('page', {}, ...linksToFollow),
|
||||||
...linksToFollow
|
...linksToFollow
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@@ -271,7 +271,7 @@ export class RemoteDataBuildService {
|
|||||||
* @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
|
||||||
*/
|
*/
|
||||||
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
buildList<T extends HALResource>(href$: string | Observable<string>, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', undefined, false, true, true, ...linksToFollow));
|
return this.buildFromHref<PaginatedList<T>>(href$, followLink('page', { shouldEmbed: false }, ...linksToFollow));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -162,6 +162,7 @@ import { UsageReport } from './statistics/models/usage-report.model';
|
|||||||
import { RootDataService } from './data/root-data.service';
|
import { RootDataService } from './data/root-data.service';
|
||||||
import { Root } from './data/root.model';
|
import { Root } from './data/root.model';
|
||||||
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
import { SearchConfig } from './shared/search/search-filters/search-config.model';
|
||||||
|
import { SequenceService } from './shared/sequence.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -282,7 +283,8 @@ const PROVIDERS = [
|
|||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
|
||||||
VocabularyService,
|
VocabularyService,
|
||||||
VocabularyTreeviewService
|
VocabularyTreeviewService,
|
||||||
|
SequenceService,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
BitstreamFormatRegistryState
|
BitstreamFormatRegistryState
|
||||||
} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
import { historyReducer, HistoryState } from './history/history.reducer';
|
import { historyReducer, HistoryState } from './history/history.reducer';
|
||||||
|
import { metaTagReducer, MetaTagState } from './metadata/meta-tag.reducer';
|
||||||
|
|
||||||
export interface CoreState {
|
export interface CoreState {
|
||||||
'bitstreamFormats': BitstreamFormatRegistryState;
|
'bitstreamFormats': BitstreamFormatRegistryState;
|
||||||
@@ -24,6 +25,7 @@ export interface CoreState {
|
|||||||
'index': MetaIndexState;
|
'index': MetaIndexState;
|
||||||
'auth': AuthState;
|
'auth': AuthState;
|
||||||
'json/patch': JsonPatchOperationsState;
|
'json/patch': JsonPatchOperationsState;
|
||||||
|
'metaTag': MetaTagState;
|
||||||
'route': RouteState;
|
'route': RouteState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,5 +39,6 @@ export const coreReducers: ActionReducerMap<CoreState> = {
|
|||||||
'index': indexReducer,
|
'index': indexReducer,
|
||||||
'auth': authReducer,
|
'auth': authReducer,
|
||||||
'json/patch': jsonPatchOperationsReducer,
|
'json/patch': jsonPatchOperationsReducer,
|
||||||
|
'metaTag': metaTagReducer,
|
||||||
'route': routeReducer
|
'route': routeReducer
|
||||||
};
|
};
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
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';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
@@ -18,7 +18,7 @@ import { Item } from '../shared/item.model';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { PaginatedList, buildPaginatedList } from './paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FindListOptions, PutRequest } from './request.models';
|
import { FindListOptions, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -28,7 +28,6 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
|||||||
import { sendRequest } from '../shared/operators';
|
import { sendRequest } from '../shared/operators';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { RequestEntryState } from './request.reducer';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
@@ -75,92 +74,6 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.findAllByHref(bundle._links.bitstreams.href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the thumbnail for the given item
|
|
||||||
* @returns {Observable<RemoteData<{@link Bitstream}>>} the first bitstream in the THUMBNAIL bundle
|
|
||||||
*/
|
|
||||||
// TODO should be implemented rest side. {@link Item} should get a thumbnail link
|
|
||||||
public getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
|
||||||
if (isNotEmpty(bundleRD.payload)) {
|
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe(
|
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
bitstreamRD.state,
|
|
||||||
bitstreamRD.errorMessage,
|
|
||||||
bitstreamRD.payload.page[0],
|
|
||||||
bitstreamRD.statusCode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return bitstreamRD as any;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [bundleRD as any];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the matching thumbnail for a {@link Bitstream}.
|
|
||||||
*
|
|
||||||
* The {@link Item} is technically redundant, but is available
|
|
||||||
* in all current use cases, and having it simplifies this method
|
|
||||||
*
|
|
||||||
* @param item The {@link Item} the {@link Bitstream} and its thumbnail are a part of
|
|
||||||
* @param bitstreamInOriginal The original {@link Bitstream} to find the thumbnail for
|
|
||||||
*/
|
|
||||||
// TODO should be implemented rest side
|
|
||||||
public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable<RemoteData<Bitstream>> {
|
|
||||||
return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe(
|
|
||||||
switchMap((bundleRD: RemoteData<Bundle>) => {
|
|
||||||
if (isNotEmpty(bundleRD.payload)) {
|
|
||||||
return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 9999 }).pipe(
|
|
||||||
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) {
|
|
||||||
const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) =>
|
|
||||||
thumbnail.name.startsWith(bitstreamInOriginal.name)
|
|
||||||
);
|
|
||||||
if (hasValue(matchingThumbnail)) {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
bitstreamRD.state,
|
|
||||||
bitstreamRD.errorMessage,
|
|
||||||
matchingThumbnail,
|
|
||||||
bitstreamRD.statusCode
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return new RemoteData(
|
|
||||||
bitstreamRD.timeCompleted,
|
|
||||||
bitstreamRD.msToLive,
|
|
||||||
bitstreamRD.lastUpdated,
|
|
||||||
RequestEntryState.Error,
|
|
||||||
'No matching thumbnail found',
|
|
||||||
undefined,
|
|
||||||
404
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return bitstreamRD as any;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [bundleRD as any];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all {@link Bitstream}s in a certain {@link Bundle}.
|
* Retrieve all {@link Bitstream}s in a certain {@link Bundle}.
|
||||||
*
|
*
|
||||||
|
@@ -233,7 +233,7 @@ describe('DataService', () => {
|
|||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
elementsPerPage: 5
|
elementsPerPage: 5
|
||||||
});
|
});
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles', config, true, true, true)).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -253,7 +253,7 @@ describe('DataService', () => {
|
|||||||
elementsPerPage: 2
|
elementsPerPage: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', config, true, true, true), followLink('templateItemOf')).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -261,7 +261,13 @@ describe('DataService', () => {
|
|||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${endpoint}?embed=templateItemOf`;
|
const expected = `${endpoint}?embed=templateItemOf`;
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf')).subscribe((value) => {
|
(service as any).getFindAllHref(
|
||||||
|
{},
|
||||||
|
null,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -269,7 +275,7 @@ describe('DataService', () => {
|
|||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
|
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships')))).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -279,7 +285,7 @@ describe('DataService', () => {
|
|||||||
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
const config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
elementsPerPage: 4
|
elementsPerPage: 4
|
||||||
});
|
});
|
||||||
(service as any).getFindAllHref({}, null, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', config, true, true, true))).subscribe((value) => {
|
(service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => {
|
||||||
expect(value).toBe(expected);
|
expect(value).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -308,13 +314,19 @@ describe('DataService', () => {
|
|||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
|
const result = (service as any).getIDHref(
|
||||||
|
endpointMock,
|
||||||
|
resourceIdMock,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`;
|
||||||
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', undefined, true, true, true,followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
|
const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships'))));
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -174,13 +174,29 @@ describe('DsoRedirectDataService', () => {
|
|||||||
|
|
||||||
it('should not include linksToFollow with shouldEmbed = false', () => {
|
it('should not include linksToFollow with shouldEmbed = false', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
const expected = `${requestUUIDURL}&embed=templateItemOf`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('bundles', undefined, false), followLink('owningCollection', undefined, false), followLink('templateItemOf'));
|
const result = (service as any).getIDHref(
|
||||||
|
pidLink,
|
||||||
|
dsoUUID,
|
||||||
|
followLink('bundles', { shouldEmbed: false }),
|
||||||
|
followLink('owningCollection', { shouldEmbed: false }),
|
||||||
|
followLink('templateItemOf')
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include nested linksToFollow 3lvl', () => {
|
it('should include nested linksToFollow 3lvl', () => {
|
||||||
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
const expected = `${requestUUIDURL}&embed=owningCollection/itemtemplate/relationships`;
|
||||||
const result = (service as any).getIDHref(pidLink, dsoUUID, followLink('owningCollection', undefined, true, true, true, followLink('itemtemplate', undefined, true, true, true, followLink('relationships'))));
|
const result = (service as any).getIDHref(
|
||||||
|
pidLink,
|
||||||
|
dsoUUID,
|
||||||
|
followLink('owningCollection',
|
||||||
|
{},
|
||||||
|
followLink('itemtemplate',
|
||||||
|
{},
|
||||||
|
followLink('relationships')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -10,6 +10,7 @@ export enum FeatureID {
|
|||||||
ReinstateItem = 'reinstateItem',
|
ReinstateItem = 'reinstateItem',
|
||||||
EPersonRegistration = 'epersonRegistration',
|
EPersonRegistration = 'epersonRegistration',
|
||||||
CanManageGroups = 'canManageGroups',
|
CanManageGroups = 'canManageGroups',
|
||||||
|
CanManageGroup = 'canManageGroup',
|
||||||
IsCollectionAdmin = 'isCollectionAdmin',
|
IsCollectionAdmin = 'isCollectionAdmin',
|
||||||
IsCommunityAdmin = 'isCommunityAdmin',
|
IsCommunityAdmin = 'isCommunityAdmin',
|
||||||
CanDownload = 'canDownload',
|
CanDownload = 'canDownload',
|
||||||
|
@@ -23,14 +23,7 @@ import { DataService } from './data.service';
|
|||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { PaginatedList } from './paginated-list.model';
|
import { PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import { DeleteRequest, FindListOptions, GetRequest, PostRequest, PutRequest, RestRequest } from './request.models';
|
||||||
DeleteRequest,
|
|
||||||
FindListOptions,
|
|
||||||
GetRequest,
|
|
||||||
PostRequest,
|
|
||||||
PutRequest,
|
|
||||||
RestRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
|
||||||
import { Bundle } from '../shared/bundle.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
@@ -38,6 +31,9 @@ import { MetadataMap } from '../shared/metadata.models';
|
|||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { NoContent } from '../shared/NoContent.model';
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(ITEM)
|
@dataService(ITEM)
|
||||||
@@ -229,7 +225,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
* @param itemId
|
* @param itemId
|
||||||
* @param collection
|
* @param collection
|
||||||
*/
|
*/
|
||||||
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<Collection>> {
|
public moveToCollection(itemId: string, collection: Collection): Observable<RemoteData<any>> {
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'text/uri-list');
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
@@ -242,9 +238,17 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PutRequest(requestId, href, collection._links.self.href, options);
|
const request = new PutRequest(requestId, href, collection._links.self.href, options);
|
||||||
this.requestService.send(request);
|
Object.assign(request, {
|
||||||
|
// TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return StatusCodeOnlyResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return request;
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe((request) => {
|
||||||
|
this.requestService.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
@@ -579,4 +579,19 @@ describe('RequestService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('uriEncodeBody', () => {
|
||||||
|
it('should properly encode the body', () => {
|
||||||
|
const body = {
|
||||||
|
'property1': 'multiple\nlines\nto\nsend',
|
||||||
|
'property2': 'sp&ci@l characters',
|
||||||
|
'sp&ci@l-chars in prop': 'test123',
|
||||||
|
};
|
||||||
|
const queryParams = service.uriEncodeBody(body);
|
||||||
|
expect(queryParams).toEqual(
|
||||||
|
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -265,11 +265,13 @@ export class RequestService {
|
|||||||
if (isNotEmpty(body) && typeof body === 'object') {
|
if (isNotEmpty(body) && typeof body === 'object') {
|
||||||
Object.keys(body)
|
Object.keys(body)
|
||||||
.forEach((param) => {
|
.forEach((param) => {
|
||||||
const paramValue = `${param}=${body[param]}`;
|
const encodedParam = encodeURIComponent(param);
|
||||||
|
const encodedBody = encodeURIComponent(body[param]);
|
||||||
|
const paramValue = `${encodedParam}=${encodedBody}`;
|
||||||
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return encodeURI(queryParams);
|
return queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user