mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'main' into w2p-71764_Features-support-part-2
Conflicts: src/app/core/core.module.ts
This commit is contained in:
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug, needs triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is. Include the version(s) of DSpace where you've seen this problem & what *web browser* you were using. Link to examples if they are public.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Do this
|
||||||
|
2. Then this...
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Related work**
|
||||||
|
Link to any related tickets or PRs here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a new feature for this project
|
||||||
|
title: ''
|
||||||
|
labels: new feature, needs triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives or workarounds you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
26
.github/disabled-workflows/pull_request_opened.yml
vendored
Normal file
26
.github/disabled-workflows/pull_request_opened.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# This workflow runs whenever a new pull request is created
|
||||||
|
# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs).
|
||||||
|
# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818
|
||||||
|
name: Pull Request opened
|
||||||
|
|
||||||
|
# Only run for newly opened PRs against the "main" branch
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
|
||||||
|
# See https://github.com/marketplace/actions/pull-request-assigner
|
||||||
|
- name: Assign PR to creator
|
||||||
|
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
|
||||||
|
# Note, this authentication token is created automatically
|
||||||
|
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Ignore errors. It is possible the PR was created by someone who cannot be assigned
|
||||||
|
continue-on-error: true
|
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
@@ -1,7 +1,7 @@
|
|||||||
## References
|
## References
|
||||||
_Add references/links to any related tickets or PRs. These may include:_
|
_Add references/links to any related issues or PRs. These may include:_
|
||||||
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
* Fixes [GitHub issue](https://github.com/DSpace/dspace-angular/issues), if any
|
||||||
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
* Requires [REST API PR](https://github.com/DSpace/DSpace/pulls), if any
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
Short summary of changes (1-2 sentences).
|
Short summary of changes (1-2 sentences).
|
||||||
@@ -25,4 +25,4 @@ _This checklist provides a reminder of what we are going to look for when review
|
|||||||
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||||
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||||
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||||
|
29
.github/workflows/issue_opened.yml
vendored
Normal file
29
.github/workflows/issue_opened.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# This workflow runs whenever a new issue is created
|
||||||
|
name: Issue opened
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Add the new issue to a project board, if it needs triage
|
||||||
|
# See https://github.com/marketplace/actions/create-project-card-action
|
||||||
|
- name: Add issue to project board
|
||||||
|
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
||||||
|
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
||||||
|
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
||||||
|
uses: technote-space/create-project-card-action@v1
|
||||||
|
# Note, the authentication token below is an ORG level Secret.
|
||||||
|
# It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
|
||||||
|
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
||||||
|
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
|
||||||
|
PROJECT: DSpace Backlog
|
||||||
|
COLUMN: Triage
|
||||||
|
CHECK_ORG_PROJECT: true
|
||||||
|
# Ignore errors
|
||||||
|
continue-on-error: true
|
17
.travis.yml
17
.travis.yml
@@ -1,4 +1,4 @@
|
|||||||
sudo: required
|
os: linux
|
||||||
dist: bionic
|
dist: bionic
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
@@ -35,10 +35,11 @@ before_install:
|
|||||||
- google-chrome-stable --version
|
- google-chrome-stable --version
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# Start up DSpace 7 using the entities database dump
|
# Start up a test DSpace 7 REST backend using the entities database dump
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||||
# Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update
|
# Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update
|
||||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
|
# Install all local dependencies (retry if initially fails)
|
||||||
- travis_retry yarn install
|
- travis_retry yarn install
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
@@ -49,9 +50,17 @@ before_script:
|
|||||||
#- curl http://localhost:8080/server/
|
#- curl http://localhost:8080/server/
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- yarn run ci
|
# build app and run all tests
|
||||||
- cat coverage/dspace-angular-cli/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
- ng lint
|
||||||
|
- travis_wait yarn run build:prod
|
||||||
|
- yarn test:headless
|
||||||
|
- yarn run e2e:ci
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
# Shutdown docker after everything runs
|
# Shutdown docker after everything runs
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||||
|
|
||||||
|
# After a successful build and test (see 'script'), send code coverage reports to coveralls.io
|
||||||
|
# These code coverage reports are generated by the coveralls node module in our package.json
|
||||||
|
after_success:
|
||||||
|
- cat coverage/dspace-angular/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
[](https://travis-ci.org/DSpace/dspace-angular) [](https://coveralls.io/github/DSpace/dspace-angular?branch=master) [](https://github.com/angular/universal)
|
[](https://travis-ci.com/DSpace/dspace-angular) [](https://coveralls.io/github/DSpace/dspace-angular?branch=main) [](https://github.com/angular/universal)
|
||||||
|
|
||||||
dspace-angular
|
dspace-angular
|
||||||
==============
|
==============
|
||||||
|
16
angular.json
16
angular.json
@@ -3,7 +3,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"dspace-angular-cli": {
|
"dspace-angular": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"path": "./webpack/webpack.common.ts",
|
"path": "./webpack/webpack.common.ts",
|
||||||
"mergeStrategies": {
|
"mergeStrategies": {
|
||||||
"loaders": "prepend"
|
"loaders": "prepend"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"outputPath": "dist/browser",
|
"outputPath": "dist/browser",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
@@ -65,19 +65,19 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular-cli:build",
|
"browserTarget": "dspace-angular:build",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular-cli:build:production"
|
"browserTarget": "dspace-angular:build:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular-cli:build"
|
"browserTarget": "dspace-angular:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
@@ -119,11 +119,11 @@
|
|||||||
"builder": "@angular-devkit/build-angular:protractor",
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
"options": {
|
"options": {
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
"devServerTarget": "dspace-angular-cli:serve"
|
"devServerTarget": "dspace-angular:serve"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"devServerTarget": "dspace-angular-cli:serve:production"
|
"devServerTarget": "dspace-angular:serve:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -153,5 +153,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "dspace-angular-cli"
|
"defaultProject": "dspace-angular"
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ version: "3.7"
|
|||||||
services:
|
services:
|
||||||
dspace-cli:
|
dspace-cli:
|
||||||
environment:
|
environment:
|
||||||
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/master/dogAndReport.zip
|
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip
|
||||||
- ADMIN_EMAIL=test@test.edu
|
- ADMIN_EMAIL=test@test.edu
|
||||||
- AIPDIR=/tmp/aip-dir
|
- AIPDIR=/tmp/aip-dir
|
||||||
entrypoint:
|
entrypoint:
|
||||||
|
@@ -17,7 +17,7 @@ module.exports = function (config) {
|
|||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
},
|
},
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
dir: require('path').join(__dirname, './coverage/dspace-angular-cli'),
|
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
fixWebpackSourcePaths: true
|
fixWebpackSourcePaths: true
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular-cli",
|
"name": "dspace-angular",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"build:prod": "yarn run build:ssr",
|
"build:prod": "yarn run build:ssr",
|
||||||
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
|
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
|
||||||
"build:client-and-server-bundles": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng build --prod && ng run dspace-angular-cli:server:production --bundleDependencies all",
|
"build:client-and-server-bundles": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng build --prod && ng run dspace-angular:server:production --bundleDependencies all",
|
||||||
"test:watch": "npm-run-all --parallel config:test:watch test",
|
"test:watch": "npm-run-all --parallel config:test:watch test",
|
||||||
"test": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --sourceMap=true --watch=true",
|
"test": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --sourceMap=true --watch=true",
|
||||||
"test:headless": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
"e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js",
|
"e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js",
|
||||||
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
|
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
|
||||||
"serve:ssr": "node dist/server",
|
"serve:ssr": "node dist/server",
|
||||||
"ci": "ng lint && yarn run build:prod && yarn test:headless && yarn run e2e:ci",
|
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
"clean:dist": "rimraf dist",
|
"clean:dist": "rimraf dist",
|
||||||
"clean:doc": "rimraf doc",
|
"clean:doc": "rimraf doc",
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
"codelyzer": "^5.0.0",
|
"codelyzer": "^5.0.0",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
"compression-webpack-plugin": "^3.0.1",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"coveralls": "3.0.0",
|
"coveralls": "^3.0.0",
|
||||||
"css-loader": "3.4.0",
|
"css-loader": "3.4.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2>{{'admin.curation-tasks.header' |translate }}</h2>
|
||||||
|
<ds-curation-form></ds-curation-form>
|
||||||
|
</div>
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { AdminCurationTasksComponent } from './admin-curation-tasks.component';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
describe('AdminCurationTasksComponent', () => {
|
||||||
|
let comp: AdminCurationTasksComponent;
|
||||||
|
let fixture: ComponentFixture<AdminCurationTasksComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [AdminCurationTasksComponent],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminCurationTasksComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
describe('init', () => {
|
||||||
|
it('should initialise the comp', () => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
expect(fixture.debugElement.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the system wide Curation Task UI
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-admin-curation-task',
|
||||||
|
templateUrl: './admin-curation-tasks.component.html',
|
||||||
|
})
|
||||||
|
export class AdminCurationTasksComponent {
|
||||||
|
|
||||||
|
}
|
@@ -209,7 +209,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
selectBitstreamFormat: {},
|
selectBitstreamFormat: {},
|
||||||
deselectBitstreamFormat: {},
|
deselectBitstreamFormat: {},
|
||||||
deselectAllBitstreamFormats: {},
|
deselectAllBitstreamFormats: {},
|
||||||
delete: observableOf(true),
|
delete: observableOf({ isSuccessful: true }),
|
||||||
clearBitStreamFormatRequests: observableOf('cleared')
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ import { hasValue } from '../../../shared/empty.util';
|
|||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a list of bitstream formats
|
* This component renders a list of bitstream formats
|
||||||
@@ -64,7 +65,7 @@ export class BitstreamFormatsComponent implements OnInit {
|
|||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
for (const format of formats) {
|
for (const format of formats) {
|
||||||
if (hasValue(format.id)) {
|
if (hasValue(format.id)) {
|
||||||
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RestResponse) => response.isSuccessful)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||||
|
@@ -6,6 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
|
|
||||||
const REGISTRIES_MODULE_PATH = 'registries';
|
const REGISTRIES_MODULE_PATH = 'registries';
|
||||||
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||||
@@ -41,6 +42,12 @@ export function getAccessControlModulePath() {
|
|||||||
component: AdminWorkflowPageComponent,
|
component: AdminWorkflowPageComponent,
|
||||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'curation-tasks',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: AdminCurationTasksComponent,
|
||||||
|
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -469,7 +469,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.curation_task',
|
text: 'menu.section.curation_task',
|
||||||
link: ''
|
link: 'admin/curation-tasks'
|
||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
icon: 'filter',
|
icon: 'filter',
|
||||||
index: 7
|
index: 7
|
||||||
|
@@ -16,6 +16,7 @@ import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './adm
|
|||||||
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
||||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||||
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -35,6 +36,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
|||||||
CommunityAdminSearchResultGridElementComponent,
|
CommunityAdminSearchResultGridElementComponent,
|
||||||
CollectionAdminSearchResultGridElementComponent,
|
CollectionAdminSearchResultGridElementComponent,
|
||||||
ItemAdminSearchResultActionsComponent,
|
ItemAdminSearchResultActionsComponent,
|
||||||
|
AdminCurationTasksComponent,
|
||||||
|
|
||||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||||
|
@@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
|
|||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
import { map, tap, find } from 'rxjs/operators';
|
||||||
import { map, tap } from 'rxjs/operators';
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,12 +28,9 @@ export class CreateCollectionPageGuard implements CanActivate {
|
|||||||
this.router.navigate(['/404']);
|
this.router.navigate(['/404']);
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
}
|
}
|
||||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
return this.communityService.findById(parentID)
|
||||||
.pipe(
|
.pipe(
|
||||||
getFinishedRemoteData(),
|
find((communityRD: RemoteData<Community>) => hasValue(communityRD.payload) || hasValue(communityRD.error)),
|
||||||
);
|
|
||||||
|
|
||||||
return parent.pipe(
|
|
||||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
tap((isValid: boolean) => {
|
tap((isValid: boolean) => {
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h3>{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}</h3>
|
||||||
|
<ds-curation-form
|
||||||
|
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||||
|
></ds-curation-form>
|
||||||
|
</div>
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
|
import { CollectionCurateComponent } from './collection-curate.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
describe('CollectionCurateComponent', () => {
|
||||||
|
let comp: CollectionCurateComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionCurateComponent>;
|
||||||
|
let debugEl: DebugElement;
|
||||||
|
|
||||||
|
let routeStub;
|
||||||
|
let dsoNameService;
|
||||||
|
|
||||||
|
const collection = Object.assign(new Collection(), {
|
||||||
|
handle: '123456789/1', metadata: {'dc.title': ['Collection Name']}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
routeStub = {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||||
|
getName: 'Collection Name'
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [CollectionCurateComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: DSONameService, useValue: dsoNameService}
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CollectionCurateComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
debugEl = fixture.debugElement;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('init', () => {
|
||||||
|
it('should initialise the comp', () => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
expect(debugEl.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||||
|
});
|
||||||
|
it('should contain the collection information provided in the route', () => {
|
||||||
|
comp.dsoRD$.subscribe((value) => {
|
||||||
|
expect(value.payload.handle
|
||||||
|
).toEqual('123456789/1');
|
||||||
|
});
|
||||||
|
comp.collectionName$.subscribe((value) => {
|
||||||
|
expect(value).toEqual('Collection Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,4 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for managing a collection's curation tasks
|
* Component for managing a collection's curation tasks
|
||||||
@@ -8,5 +15,26 @@ import { Component } from '@angular/core';
|
|||||||
templateUrl: './collection-curate.component.html',
|
templateUrl: './collection-curate.component.html',
|
||||||
})
|
})
|
||||||
export class CollectionCurateComponent {
|
export class CollectionCurateComponent {
|
||||||
/* TODO: Implement Collection Edit - Curate */
|
dsoRD$: Observable<RemoteData<Collection>>;
|
||||||
|
collectionName$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private dsoNameService: DSONameService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dsoRD$ = this.route.parent.data.pipe(
|
||||||
|
take(1),
|
||||||
|
map((data) => data.dso),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.collectionName$ = this.dsoRD$.pipe(
|
||||||
|
filter((rd: RemoteData<Collection>) => hasValue(rd)),
|
||||||
|
map((rd: RemoteData<Collection>) => {
|
||||||
|
return this.dsoNameService.getName(rd.payload);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
|
|||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
import { map, tap, find } from 'rxjs/operators';
|
||||||
import { map, tap } from 'rxjs/operators';
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,18 +28,16 @@ export class CreateCommunityPageGuard implements CanActivate {
|
|||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
return this.communityService.findById(parentID)
|
||||||
.pipe(
|
.pipe(
|
||||||
getFinishedRemoteData(),
|
find((communityRD: RemoteData<Community>) => hasValue(communityRD.payload) || hasValue(communityRD.error)),
|
||||||
);
|
|
||||||
|
|
||||||
return parent.pipe(
|
|
||||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
tap((isValid: boolean) => {
|
tap((isValid: boolean) => {
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
this.router.navigate(['/404']);
|
this.router.navigate(['/404']);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h3>{{'community.curate.header' |translate:{community: (communityName$ |async)} }}</h3>
|
||||||
|
<ds-curation-form
|
||||||
|
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||||
|
></ds-curation-form>
|
||||||
|
</div>
|
||||||
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { CommunityCurateComponent } from './community-curate.component';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
|
||||||
|
describe('CommunityCurateComponent', () => {
|
||||||
|
let comp: CommunityCurateComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityCurateComponent>;
|
||||||
|
let debugEl: DebugElement;
|
||||||
|
|
||||||
|
let routeStub;
|
||||||
|
let dsoNameService;
|
||||||
|
|
||||||
|
const community = Object.assign(new Community(), {
|
||||||
|
handle: '123456789/1', metadata: {'dc.title': ['Community Name']}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
routeStub = {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(community)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dsoNameService = jasmine.createSpyObj('dsoNameService', {
|
||||||
|
getName: 'Community Name'
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [CommunityCurateComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: DSONameService, useValue: dsoNameService}
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CommunityCurateComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
debugEl = fixture.debugElement;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('init', () => {
|
||||||
|
it('should initialise the comp', () => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
expect(debugEl.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||||
|
});
|
||||||
|
it('should contain the community information provided in the route', () => {
|
||||||
|
comp.dsoRD$.subscribe((value) => {
|
||||||
|
expect(value.payload.handle
|
||||||
|
).toEqual('123456789/1');
|
||||||
|
});
|
||||||
|
comp.communityName$.subscribe((value) => {
|
||||||
|
expect(value).toEqual('Community Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,4 +1,12 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { filter } from 'rxjs/internal/operators/filter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for managing a community's curation tasks
|
* Component for managing a community's curation tasks
|
||||||
@@ -7,6 +15,29 @@ import { Component } from '@angular/core';
|
|||||||
selector: 'ds-community-curate',
|
selector: 'ds-community-curate',
|
||||||
templateUrl: './community-curate.component.html',
|
templateUrl: './community-curate.component.html',
|
||||||
})
|
})
|
||||||
export class CommunityCurateComponent {
|
export class CommunityCurateComponent implements OnInit {
|
||||||
/* TODO: Implement Community Edit - Curate */
|
|
||||||
|
dsoRD$: Observable<RemoteData<Community>>;
|
||||||
|
communityName$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private dsoNameService: DSONameService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.dsoRD$ = this.route.parent.data.pipe(
|
||||||
|
take(1),
|
||||||
|
map((data) => data.dso),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.communityName$ = this.dsoRD$.pipe(
|
||||||
|
filter((rd: RemoteData<Community>) => hasValue(rd)),
|
||||||
|
map((rd: RemoteData<Community>) => {
|
||||||
|
return this.dsoNameService.getName(rd.payload);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -114,7 +114,7 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||||
deleteAndReturnResponse: jasmine.createSpy('deleteAndReturnResponse')
|
delete: jasmine.createSpy('delete')
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
remove: jasmine.createSpy('remove')
|
remove: jasmine.createSpy('remove')
|
||||||
@@ -182,12 +182,25 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
comp.submit();
|
comp.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call deleteAndReturnResponse on the bitstreamService for the marked field', () => {
|
it('should call delete on the bitstreamService for the marked field', () => {
|
||||||
expect(bitstreamService.deleteAndReturnResponse).toHaveBeenCalledWith(bitstream2.id);
|
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
it('should not call delete on the bitstreamService for the unmarked field', () => {
|
||||||
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when dropBitstream is called', () => {
|
||||||
|
const event = {
|
||||||
|
fromIndex: 0,
|
||||||
|
toIndex: 50,
|
||||||
|
// tslint:disable-next-line:no-empty
|
||||||
|
finish: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.dropBitstream(bundle, event);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -165,7 +165,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
take(1),
|
take(1),
|
||||||
switchMap((removedBistreams: Bitstream[]) => {
|
switchMap((removedBistreams: Bitstream[]) => {
|
||||||
if (isNotEmpty(removedBistreams)) {
|
if (isNotEmpty(removedBistreams)) {
|
||||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream.id)));
|
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
|
||||||
} else {
|
} else {
|
||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,27 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import {filter, first, map, switchMap, take} from 'rxjs/operators';
|
import { defaultIfEmpty, filter, first, map, switchMap, take } from 'rxjs/operators';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {combineLatest as observableCombineLatest, combineLatest, Observable} from 'rxjs';
|
import { combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import {VirtualMetadata} from '../virtual-metadata/virtual-metadata.component';
|
import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
|
||||||
import {Relationship} from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import {hasValue, isNotEmpty} from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {MetadataValue} from '../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||||
import {ViewMode} from '../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../core/shared/view-mode.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 {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import {RelationshipService} from '../../../core/data/relationship.service';
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
|
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-delete',
|
selector: 'ds-item-delete',
|
||||||
@@ -80,6 +83,7 @@ export class ItemDeleteComponent
|
|||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected relationshipService: RelationshipService,
|
protected relationshipService: RelationshipService,
|
||||||
protected entityTypeService: EntityTypeService,
|
protected entityTypeService: EntityTypeService,
|
||||||
|
protected linkService: LinkService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
route,
|
route,
|
||||||
@@ -98,9 +102,9 @@ export class ItemDeleteComponent
|
|||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.url = this.router.url;
|
this.url = this.router.url;
|
||||||
|
|
||||||
this.types$ = this.entityTypeService.getEntityTypeByLabel(
|
const label = this.item.firstMetadataValue('relationship.type');
|
||||||
this.item.firstMetadataValue('relationship.type')
|
if (label !== undefined) {
|
||||||
).pipe(
|
this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
|
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
|
||||||
@@ -122,6 +126,9 @@ export class ItemDeleteComponent
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.types$ = observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
this.types$.pipe(
|
this.types$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
@@ -187,6 +194,7 @@ export class ItemDeleteComponent
|
|||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
relationships.map((relationship) => this.getRelationshipType(relationship))
|
relationships.map((relationship) => this.getRelationshipType(relationship))
|
||||||
).pipe(
|
).pipe(
|
||||||
|
defaultIfEmpty([]),
|
||||||
map((types) => relationships.filter(
|
map((types) => relationships.filter(
|
||||||
(relationship, index) => relationshipType.id === types[index].id
|
(relationship, index) => relationshipType.id === types[index].id
|
||||||
)),
|
)),
|
||||||
@@ -205,6 +213,12 @@ export class ItemDeleteComponent
|
|||||||
*/
|
*/
|
||||||
private getRelationshipType(relationship: Relationship): Observable<RelationshipType> {
|
private getRelationshipType(relationship: Relationship): Observable<RelationshipType> {
|
||||||
|
|
||||||
|
this.linkService.resolveLinks(
|
||||||
|
relationship,
|
||||||
|
followLink('relationshipType'),
|
||||||
|
followLink('leftItem'),
|
||||||
|
followLink('rightItem'),
|
||||||
|
);
|
||||||
return relationship.relationshipType.pipe(
|
return relationship.relationshipType.pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
@@ -305,6 +319,7 @@ export class ItemDeleteComponent
|
|||||||
combineLatest(
|
combineLatest(
|
||||||
types.map((type) => this.isSelected(type))
|
types.map((type) => this.isSelected(type))
|
||||||
).pipe(
|
).pipe(
|
||||||
|
defaultIfEmpty([]),
|
||||||
map((selection) => types.filter(
|
map((selection) => types.filter(
|
||||||
(type, index) => selection[index]
|
(type, index) => selection[index]
|
||||||
)),
|
)),
|
||||||
@@ -313,8 +328,8 @@ export class ItemDeleteComponent
|
|||||||
),
|
),
|
||||||
).subscribe((types) => {
|
).subscribe((types) => {
|
||||||
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
|
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
|
||||||
(succeeded: boolean) => {
|
(response: RestResponse) => {
|
||||||
this.notify(succeeded);
|
this.notify(response.isSuccessful);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
<div class="item-relationships">
|
<div class="item-relationships">
|
||||||
|
<ng-container *ngVar="entityType$ | async as entityType">
|
||||||
|
<ng-container *ngIf="entityType">
|
||||||
<div class="button-row top d-flex">
|
<div class="button-row top d-flex">
|
||||||
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)"
|
||||||
[disabled]="!(hasChanges() | async)"
|
[disabled]="!(hasChanges() | async)"
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
<ds-edit-relationship-list
|
<ds-edit-relationship-list
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[itemType]="entityType$ | async"
|
[itemType]="entityType"
|
||||||
[relationshipType]="relationshipType"
|
[relationshipType]="relationshipType"
|
||||||
></ds-edit-relationship-list>
|
></ds-edit-relationship-list>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,4 +47,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<div *ngIf="!entityType"
|
||||||
|
class="alert alert-info mt-2" role="alert">
|
||||||
|
{{ 'item.edit.relationships.no-entity-type' | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,7 +3,7 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
import { DeleteRelationship, FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { zip as observableZip } from 'rxjs';
|
import { of as observableOf, zip as observableZip} from 'rxjs';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -87,9 +87,10 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
*/
|
*/
|
||||||
public initializeUpdates(): void {
|
public initializeUpdates(): void {
|
||||||
|
|
||||||
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(
|
const label = this.item.firstMetadataValue('relationship.type');
|
||||||
this.item.firstMetadataValue('relationship.type')
|
if (label !== undefined) {
|
||||||
).pipe(
|
|
||||||
|
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
@@ -107,6 +108,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.entityType$ = observableOf(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
|
|||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { getFinishedRemoteData, getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,16 +75,19 @@ export const paginatedRelationsToItems = (thisId: string) =>
|
|||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest(
|
||||||
...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem.pipe(getFinishedRemoteData()), rel.rightItem.pipe(getFinishedRemoteData())))
|
relationshipsRD.payload.page.map((rel: Relationship) =>
|
||||||
).pipe(
|
observableCombineLatest([
|
||||||
|
rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())]
|
||||||
|
)
|
||||||
|
)).pipe(
|
||||||
map((arr) =>
|
map((arr) =>
|
||||||
arr
|
arr
|
||||||
.filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded)
|
|
||||||
.map(([leftItem, rightItem]) => {
|
.map(([leftItem, rightItem]) => {
|
||||||
if (leftItem.payload.id === thisId) {
|
if (leftItem.id === thisId) {
|
||||||
return rightItem.payload;
|
return rightItem;
|
||||||
} else if (rightItem.payload.id === thisId) {
|
} else if (rightItem.id === thisId) {
|
||||||
return leftItem.payload;
|
return leftItem;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((item: Item) => hasValue(item))
|
.filter((item: Item) => hasValue(item))
|
||||||
|
@@ -7,6 +7,8 @@ import { RouteService } from '../../core/services/route.service';
|
|||||||
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 { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-workflow-item-delete',
|
selector: 'ds-workflow-item-delete',
|
||||||
@@ -39,6 +41,6 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent
|
|||||||
*/
|
*/
|
||||||
sendRequest(id: string): Observable<boolean> {
|
sendRequest(id: string): Observable<boolean> {
|
||||||
this.requestService.removeByHrefSubstring('/discover');
|
this.requestService.removeByHrefSubstring('/discover');
|
||||||
return this.workflowItemService.delete(id);
|
return this.workflowItemService.delete(id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,22 @@
|
|||||||
import {
|
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
async,
|
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
ComponentFixture,
|
|
||||||
inject,
|
|
||||||
TestBed
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
|
||||||
DebugElement
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
|
||||||
// Load the implementations that should be tested
|
// Load the implementations that should be tested
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
|
||||||
import { HostWindowState } from './shared/search/host-window.reducer';
|
import { HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
|
|
||||||
import { MetadataService } from './core/metadata/metadata.service';
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
|
|
||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
|
|
||||||
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
||||||
import { MetadataServiceMock } from './shared/mocks/metadata-service.mock';
|
import { MetadataServiceMock } from './shared/mocks/metadata-service.mock';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
|
||||||
import { AngularticsMock } from './shared/mocks/angulartics.service.mock';
|
import { AngularticsMock } from './shared/mocks/angulartics.service.mock';
|
||||||
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
@@ -39,14 +26,12 @@ import { CSSVariableServiceStub } from './shared/testing/css-variable-service.st
|
|||||||
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
import { HostWindowServiceStub } from './shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from './shared/testing/host-window-service.stub';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { RouteService } from './core/services/route.service';
|
import { RouteService } from './core/services/route.service';
|
||||||
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
||||||
import { RouterMock } from './shared/mocks/router.mock';
|
import { RouterMock } from './shared/mocks/router.mock';
|
||||||
import { CookieServiceMock } from './shared/mocks/cookie.service.mock';
|
|
||||||
import { CookieService } from './core/services/cookie.service';
|
|
||||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||||
import { storeModuleConfig } from './app.reducer';
|
import { storeModuleConfig } from './app.reducer';
|
||||||
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -56,6 +41,12 @@ const menuService = new MenuServiceStub();
|
|||||||
|
|
||||||
describe('App component', () => {
|
describe('App component', () => {
|
||||||
|
|
||||||
|
function getMockLocaleService(): LocaleService {
|
||||||
|
return jasmine.createSpyObj('LocaleService', {
|
||||||
|
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// async beforeEach
|
// async beforeEach
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
@@ -81,7 +72,7 @@ describe('App component', () => {
|
|||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
{ provide: CookieService, useValue: new CookieServiceMock()},
|
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||||
AppComponent,
|
AppComponent,
|
||||||
RouteService
|
RouteService
|
||||||
],
|
],
|
||||||
|
@@ -1,10 +1,19 @@
|
|||||||
import { delay, filter, map, take } from 'rxjs/operators';
|
import { delay, filter, map, take } from 'rxjs/operators';
|
||||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, ViewEncapsulation } from '@angular/core';
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostListener,
|
||||||
|
Inject,
|
||||||
|
OnInit,
|
||||||
|
ViewEncapsulation
|
||||||
|
} from '@angular/core';
|
||||||
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
|
|
||||||
import { MetadataService } from './core/metadata/metadata.service';
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
@@ -12,20 +21,17 @@ import { HostWindowState } from './shared/search/host-window.reducer';
|
|||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
import { isAuthenticated } from './core/auth/selectors';
|
import { isAuthenticated } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
|
||||||
import variables from '../styles/_exposed_variables.scss';
|
import variables from '../styles/_exposed_variables.scss';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
import { MenuID } from './shared/menu/initial-menus-state';
|
import { MenuID } from './shared/menu/initial-menus-state';
|
||||||
import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
|
||||||
import { slideSidebarPadding } from './shared/animations/slide';
|
import { slideSidebarPadding } from './shared/animations/slide';
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
import { Theme } from '../config/theme.inferface';
|
import { Theme } from '../config/theme.inferface';
|
||||||
import { isNotEmpty } from './shared/empty.util';
|
|
||||||
import { CookieService } from './core/services/cookie.service';
|
|
||||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
|
||||||
export const LANG_COOKIE = 'language_cookie';
|
export const LANG_COOKIE = 'language_cookie';
|
||||||
|
|
||||||
@@ -59,7 +65,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private cookie: CookieService
|
private localeService: LocaleService
|
||||||
) {
|
) {
|
||||||
/* Use models object so all decorators are actually called */
|
/* Use models object so all decorators are actually called */
|
||||||
this.models = models;
|
this.models = models;
|
||||||
@@ -67,23 +73,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
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));
|
||||||
|
|
||||||
// Load the default language from the config file
|
// Load the default language from the config file
|
||||||
translate.setDefaultLang(environment.defaultLanguage);
|
// translate.setDefaultLang(environment.defaultLanguage);
|
||||||
|
|
||||||
// Attempt to get the language from a cookie
|
// set the current language code
|
||||||
const lang = cookie.get(LANG_COOKIE);
|
this.localeService.setCurrentLanguageCode();
|
||||||
if (isNotEmpty(lang)) {
|
|
||||||
// Cookie found
|
|
||||||
// Use the language from the cookie
|
|
||||||
translate.use(lang);
|
|
||||||
} else {
|
|
||||||
// Cookie not found
|
|
||||||
// Attempt to get the browser language from the user
|
|
||||||
if (translate.getLangs().includes(translate.getBrowserLang())) {
|
|
||||||
translate.use(translate.getBrowserLang());
|
|
||||||
} else {
|
|
||||||
translate.use(environment.defaultLanguage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
angulartics2GoogleAnalytics.startTracking();
|
angulartics2GoogleAnalytics.startTracking();
|
||||||
angulartics2DSpace.startTracking();
|
angulartics2DSpace.startTracking();
|
||||||
|
@@ -241,6 +241,12 @@ describe('AuthService test', () => {
|
|||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return true when authentication is loaded', () => {
|
||||||
|
authService.isAuthenticationLoaded().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
|
@@ -21,7 +21,8 @@ import {
|
|||||||
getAuthenticationToken,
|
getAuthenticationToken,
|
||||||
getRedirectUrl,
|
getRedirectUrl,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isTokenRefreshing
|
isTokenRefreshing,
|
||||||
|
isAuthenticatedLoaded
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { AppState, routerStateSelector } from '../../app.reducer';
|
import { AppState, routerStateSelector } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
@@ -148,6 +149,14 @@ export class AuthService {
|
|||||||
return this.store.pipe(select(isAuthenticated));
|
return this.store.pipe(select(isAuthenticated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if authentication is loaded
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public isAuthenticationLoaded(): Observable<boolean> {
|
||||||
|
return this.store.pipe(select(isAuthenticatedLoaded));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the href link to authenticated user
|
* Returns the href link to authenticated user
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
@@ -27,9 +27,6 @@ export class ServerAuthService extends AuthService {
|
|||||||
|
|
||||||
headers = headers.append('Accept', 'application/json');
|
headers = headers.append('Accept', 'application/json');
|
||||||
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
|
||||||
// NB this is used to pass server client IP check.
|
|
||||||
const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress;
|
|
||||||
headers = headers.append('X-Forwarded-For', clientIp);
|
|
||||||
|
|
||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
return this.authRequestService.getRequest('status', options).pipe(
|
return this.authRequestService.getRequest('status', options).pipe(
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import {
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util';
|
||||||
hasValue,
|
|
||||||
hasValueOperator,
|
|
||||||
isEmpty,
|
|
||||||
isNotEmpty,
|
|
||||||
isNotUndefined
|
|
||||||
} from '../../../shared/empty.util';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
@@ -15,12 +9,7 @@ import { RemoteData } from '../../data/remote-data';
|
|||||||
import { RemoteDataError } from '../../data/remote-data-error';
|
import { RemoteDataError } from '../../data/remote-data-error';
|
||||||
import { RequestEntry } from '../../data/request.reducer';
|
import { RequestEntry } from '../../data/request.reducer';
|
||||||
import { RequestService } from '../../data/request.service';
|
import { RequestService } from '../../data/request.service';
|
||||||
import {
|
import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators';
|
||||||
filterSuccessfulResponses,
|
|
||||||
getRequestFromRequestHref,
|
|
||||||
getRequestFromRequestUUID,
|
|
||||||
getResourceLinksFromResponse
|
|
||||||
} from '../../shared/operators';
|
|
||||||
import { PageInfo } from '../../shared/page-info.model';
|
import { PageInfo } from '../../shared/page-info.model';
|
||||||
import { CacheableObject } from '../object-cache.reducer';
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
@@ -98,7 +87,8 @@ export class RemoteDataBuildService {
|
|||||||
let error: RemoteDataError;
|
let error: RemoteDataError;
|
||||||
const response = reqEntry ? reqEntry.response : undefined;
|
const response = reqEntry ? reqEntry.response : undefined;
|
||||||
if (hasValue(response)) {
|
if (hasValue(response)) {
|
||||||
isSuccessful = response.isSuccessful;
|
isSuccessful = response.statusCode === 204 ||
|
||||||
|
response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload);
|
||||||
const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined;
|
const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined;
|
||||||
if (hasValue(errorMessage)) {
|
if (hasValue(errorMessage)) {
|
||||||
error = new RemoteDataError(
|
error = new RemoteDataError(
|
||||||
@@ -155,7 +145,7 @@ export class RemoteDataBuildService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload$ = observableCombineLatest(tDomainList$, pageInfo$).pipe(
|
const payload$ = observableCombineLatest([tDomainList$, pageInfo$]).pipe(
|
||||||
map(([tDomainList, pageInfo]) => {
|
map(([tDomainList, pageInfo]) => {
|
||||||
return new PaginatedList(pageInfo, tDomainList);
|
return new PaginatedList(pageInfo, tDomainList);
|
||||||
})
|
})
|
||||||
|
9
src/app/core/cache/object-cache.reducer.ts
vendored
9
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,7 +1,5 @@
|
|||||||
import { autoserialize, deserialize } from 'cerialize';
|
|
||||||
import { HALLink } from '../shared/hal-link.model';
|
import { HALLink } from '../shared/hal-link.model';
|
||||||
import { HALResource } from '../shared/hal-resource.model';
|
import { HALResource } from '../shared/hal-resource.model';
|
||||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
|
||||||
import {
|
import {
|
||||||
ObjectCacheAction,
|
ObjectCacheAction,
|
||||||
ObjectCacheActionTypes,
|
ObjectCacheActionTypes,
|
||||||
@@ -15,12 +13,6 @@ import { CacheEntry } from './cache-entry';
|
|||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
import { applyPatch, Operation } from 'fast-json-patch';
|
import { applyPatch, Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
export enum DirtyType {
|
|
||||||
Created = 'Created',
|
|
||||||
Updated = 'Updated',
|
|
||||||
Deleted = 'Deleted'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to represent a JsonPatch
|
* An interface to represent a JsonPatch
|
||||||
*/
|
*/
|
||||||
@@ -72,6 +64,7 @@ export class ObjectCacheEntry implements CacheEntry {
|
|||||||
patches: Patch[] = [];
|
patches: Patch[] = [];
|
||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,7 +9,7 @@ import { RestRequestMethod } from '../data/rest-request-method';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* An entry in the ServerSyncBufferState
|
* An entry in the ServerSyncBufferState
|
||||||
* href: unique href of an ObjectCacheEntry
|
* href: unique href of an ServerSyncBufferEntry
|
||||||
* method: RestRequestMethod type
|
* method: RestRequestMethod type
|
||||||
*/
|
*/
|
||||||
export class ServerSyncBufferEntry {
|
export class ServerSyncBufferEntry {
|
||||||
@@ -48,6 +48,7 @@ export function serverSyncBufferReducer(state = initialState, action: ServerSync
|
|||||||
case ServerSyncBufferActionTypes.EMPTY: {
|
case ServerSyncBufferActionTypes.EMPTY: {
|
||||||
return emptyServerSyncQueue(state, action as EmptySSBAction);
|
return emptyServerSyncQueue(state, action as EmptySSBAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -177,6 +177,7 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
Object.assign(new SubmissionDefinitionModel(), {
|
Object.assign(new SubmissionDefinitionModel(), {
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
name: 'traditional',
|
name: 'traditional',
|
||||||
|
id: 'traditional',
|
||||||
type: 'submissiondefinition',
|
type: 'submissiondefinition',
|
||||||
_links: {
|
_links: {
|
||||||
sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' },
|
sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' },
|
||||||
@@ -187,6 +188,7 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
header: 'submit.progressbar.describe.stepone',
|
header: 'submit.progressbar.describe.stepone',
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
sectionType: 'submission-form',
|
sectionType: 'submission-form',
|
||||||
|
id: 'traditionalpageone',
|
||||||
visibility: {
|
visibility: {
|
||||||
main: null,
|
main: null,
|
||||||
other: 'READONLY'
|
other: 'READONLY'
|
||||||
@@ -201,6 +203,7 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
header: 'submit.progressbar.describe.steptwo',
|
header: 'submit.progressbar.describe.steptwo',
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
sectionType: 'submission-form',
|
sectionType: 'submission-form',
|
||||||
|
id: 'traditionalpagetwo',
|
||||||
visibility: {
|
visibility: {
|
||||||
main: null,
|
main: null,
|
||||||
other: 'READONLY'
|
other: 'READONLY'
|
||||||
@@ -215,6 +218,7 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
header: 'submit.progressbar.upload',
|
header: 'submit.progressbar.upload',
|
||||||
mandatory: false,
|
mandatory: false,
|
||||||
sectionType: 'upload',
|
sectionType: 'upload',
|
||||||
|
id: 'upload',
|
||||||
visibility: {
|
visibility: {
|
||||||
main: null,
|
main: null,
|
||||||
other: 'READONLY'
|
other: 'READONLY'
|
||||||
@@ -229,6 +233,7 @@ describe('ConfigResponseParsingService', () => {
|
|||||||
header: 'submit.progressbar.license',
|
header: 'submit.progressbar.license',
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
sectionType: 'license',
|
sectionType: 'license',
|
||||||
|
id: 'license',
|
||||||
visibility: {
|
visibility: {
|
||||||
main: null,
|
main: null,
|
||||||
other: 'READONLY'
|
other: 'READONLY'
|
||||||
|
@@ -6,6 +6,12 @@ import { excludeFromEquals } from '../../utilities/equals.decorators';
|
|||||||
|
|
||||||
export abstract class ConfigObject implements CacheableObject {
|
export abstract class ConfigObject implements CacheableObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name for this configuration
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
public id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name for this configuration
|
* The name for this configuration
|
||||||
*/
|
*/
|
||||||
|
@@ -144,6 +144,7 @@ import { ScriptDataService } from './data/processes/script-data.service';
|
|||||||
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
|
import { LocaleInterceptor } from './locale/locale.interceptor';
|
||||||
import { ItemTemplateDataService } from './data/item-template-data.service';
|
import { ItemTemplateDataService } from './data/item-template-data.service';
|
||||||
import { TemplateItem } from './shared/template-item.model';
|
import { TemplateItem } from './shared/template-item.model';
|
||||||
import { Feature } from './shared/feature.model';
|
import { Feature } from './shared/feature.model';
|
||||||
@@ -159,6 +160,8 @@ import { SubmissionCcLicenseDataService } from './submission/submission-cc-licen
|
|||||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||||
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
|
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
|
||||||
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
||||||
|
import { ConfigurationDataService } from './data/configuration-data.service';
|
||||||
|
import { ConfigurationProperty } from './shared/configuration-property.model';
|
||||||
import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard';
|
import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,6 +248,7 @@ const PROVIDERS = [
|
|||||||
UploaderService,
|
UploaderService,
|
||||||
FileService,
|
FileService,
|
||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
|
ConfigurationDataService,
|
||||||
DSOChangeAnalyzer,
|
DSOChangeAnalyzer,
|
||||||
DefaultChangeAnalyzer,
|
DefaultChangeAnalyzer,
|
||||||
ArrayMoveChangeAnalyzer,
|
ArrayMoveChangeAnalyzer,
|
||||||
@@ -293,6 +297,12 @@ const PROVIDERS = [
|
|||||||
useClass: AuthInterceptor,
|
useClass: AuthInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
|
// register LocaleInterceptor as HttpInterceptor
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: LocaleInterceptor,
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
FilteredDiscoveryPageResponseParsingService,
|
FilteredDiscoveryPageResponseParsingService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
@@ -345,7 +355,8 @@ export const models =
|
|||||||
TemplateItem,
|
TemplateItem,
|
||||||
Feature,
|
Feature,
|
||||||
Authorization,
|
Authorization,
|
||||||
Registration
|
Registration,
|
||||||
|
ConfigurationProperty
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -281,7 +281,7 @@ describe('BitstreamFormatDataService', () => {
|
|||||||
format.uuid = 'format-uuid';
|
format.uuid = 'format-uuid';
|
||||||
format.id = 'format-id';
|
format.id = 'format-id';
|
||||||
|
|
||||||
const expected = cold('(b|)', {b: true});
|
const expected = cold('(b|)', { b: responseCacheEntry.response });
|
||||||
const result = service.delete(format.id);
|
const result = service.delete(format.id);
|
||||||
|
|
||||||
expect(result).toBeObservable(expected);
|
expect(result).toBeObservable(expected);
|
||||||
|
@@ -155,9 +155,9 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
|||||||
/**
|
/**
|
||||||
* Delete an existing DSpace Object on the server
|
* Delete an existing DSpace Object on the server
|
||||||
* @param formatID The DSpace Object'id to be removed
|
* @param formatID The DSpace Object'id to be removed
|
||||||
* Return an observable that emits true when the deletion was successful, false when it failed
|
* @return the RestResponse as an Observable
|
||||||
*/
|
*/
|
||||||
delete(formatID: string): Observable<boolean> {
|
delete(formatID: string): Observable<RestResponse> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
@@ -173,7 +173,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
|||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
find((request: RequestEntry) => request.completed),
|
find((request: RequestEntry) => request.completed),
|
||||||
map((request: RequestEntry) => request.response.isSuccessful)
|
map((request: RequestEntry) => request.response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
94
src/app/core/data/bundle-data.service.spec.ts
Normal file
94
src/app/core/data/bundle-data.service.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { FindListOptions, PatchRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { BundleDataService } from './bundle-data.service';
|
||||||
|
import { HALLink } from '../shared/hal-link.model';
|
||||||
|
|
||||||
|
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||||
|
diff(object1: Item, object2: Item): Operation[] {
|
||||||
|
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BundleDataService', () => {
|
||||||
|
let service: BundleDataService;
|
||||||
|
let requestService;
|
||||||
|
let halService;
|
||||||
|
let rdbService;
|
||||||
|
let notificationsService;
|
||||||
|
let http;
|
||||||
|
let comparator;
|
||||||
|
let objectCache;
|
||||||
|
let store;
|
||||||
|
let item;
|
||||||
|
let bundleLink;
|
||||||
|
let bundleHALLink;
|
||||||
|
|
||||||
|
function initTestService(): BundleDataService {
|
||||||
|
bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles';
|
||||||
|
bundleHALLink = new HALLink();
|
||||||
|
bundleHALLink.href = bundleLink;
|
||||||
|
item = new Item();
|
||||||
|
item._links = {
|
||||||
|
bundles: bundleHALLink
|
||||||
|
};
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = new HALEndpointServiceStub('url') as any;
|
||||||
|
rdbService = {} as RemoteDataBuildService;
|
||||||
|
notificationsService = {} as NotificationsService;
|
||||||
|
http = {} as HttpClient;
|
||||||
|
comparator = new DummyChangeAnalyzer() as any;
|
||||||
|
objectCache = {
|
||||||
|
|
||||||
|
addPatch: () => {
|
||||||
|
/* empty */
|
||||||
|
},
|
||||||
|
getObjectBySelfLink: () => {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
store = {} as Store<CoreState>;
|
||||||
|
return new BundleDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findAllByItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'findAllByHref');
|
||||||
|
service.findAllByItem(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call findAllByHref with the item\'s bundles link', () => {
|
||||||
|
expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -73,7 +73,12 @@ describe('CollectionDataService', () => {
|
|||||||
|
|
||||||
describe('when the requests are successful', () => {
|
describe('when the requests are successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService(observableOf({
|
||||||
|
request: {
|
||||||
|
href: 'https://rest.api/request'
|
||||||
|
},
|
||||||
|
completed: true
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling getContentSource', () => {
|
describe('when calling getContentSource', () => {
|
||||||
@@ -133,7 +138,7 @@ describe('CollectionDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
||||||
const result = service.getAuthorizedCollection(queryString)
|
const result = service.getAuthorizedCollection(queryString);
|
||||||
const expected = cold('a|', {
|
const expected = cold('a|', {
|
||||||
a: paginatedListRD
|
a: paginatedListRD
|
||||||
});
|
});
|
||||||
@@ -148,7 +153,7 @@ describe('CollectionDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
||||||
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString)
|
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString);
|
||||||
const expected = cold('a|', {
|
const expected = cold('a|', {
|
||||||
a: paginatedListRD
|
a: paginatedListRD
|
||||||
});
|
});
|
||||||
|
87
src/app/core/data/configuration-data.service.spec.ts
Normal file
87
src/app/core/data/configuration-data.service.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { FindByIDRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { ConfigurationDataService } from './configuration-data.service';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
|
||||||
|
describe('ConfigurationDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: ConfigurationDataService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
const testObject = {
|
||||||
|
uuid: 'test-property',
|
||||||
|
name: 'test-property',
|
||||||
|
values: ['value-1', 'value-2']
|
||||||
|
} as ConfigurationProperty;
|
||||||
|
const configLink = 'https://rest.api/rest/api/config/properties';
|
||||||
|
const requestURL = `https://rest.api/rest/api/config/properties/${testObject.name}`;
|
||||||
|
const requestUUID = 'test-property';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
halService = jasmine.createSpyObj('halService', {
|
||||||
|
getEndpoint: cold('a', {a: configLink})
|
||||||
|
});
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
generateRequestId: requestUUID,
|
||||||
|
configure: true
|
||||||
|
});
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildSingle: cold('a', {
|
||||||
|
a: {
|
||||||
|
payload: testObject
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
objectCache = {} as ObjectCacheService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
|
||||||
|
service = new ConfigurationDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should call HALEndpointService with the path to the properties endpoint', () => {
|
||||||
|
scheduler.schedule(() => service.findByPropertyName(testObject.name));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith('properties');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure the proper FindByIDRequest', () => {
|
||||||
|
scheduler.schedule(() => service.findByPropertyName(testObject.name));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<ConfigurationProperty> for the object with the given name', () => {
|
||||||
|
const result = service.findByPropertyName(testObject.name);
|
||||||
|
const expected = cold('a', {
|
||||||
|
a: {
|
||||||
|
payload: testObject
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
62
src/app/core/data/configuration-data.service.ts
Normal file
62
src/app/core/data/configuration-data.service.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { CONFIG_PROPERTY } from '../shared/config-property.resource-type';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
class DataServiceImpl extends DataService<ConfigurationProperty> {
|
||||||
|
protected linkPath = 'properties';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ConfigurationProperty>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(CONFIG_PROPERTY)
|
||||||
|
/**
|
||||||
|
* Data Service responsible for retrieving Configuration properties
|
||||||
|
*/
|
||||||
|
export class ConfigurationDataService {
|
||||||
|
protected linkPath = 'properties';
|
||||||
|
private dataService: DataServiceImpl;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ConfigurationProperty>) {
|
||||||
|
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a configuration property by name
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
findByPropertyName(name: string): Observable<RemoteData<ConfigurationProperty>> {
|
||||||
|
return this.dataService.findById(name);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,19 +2,8 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
distinctUntilChanged,
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
filter,
|
|
||||||
find,
|
|
||||||
first,
|
|
||||||
map,
|
|
||||||
mergeMap,
|
|
||||||
skipWhile,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
tap
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
@@ -28,25 +17,12 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import {
|
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||||
configureRequest,
|
|
||||||
getRemoteDataPayload,
|
|
||||||
getResponseFromEntry,
|
|
||||||
getSucceededRemoteData
|
|
||||||
} from '../shared/operators';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import {
|
import { CreateRequest, DeleteByIDRequest, FindByIDRequest, FindListOptions, FindListRequest, GetRequest, PatchRequest, PutRequest } from './request.models';
|
||||||
CreateRequest,
|
|
||||||
DeleteByIDRequest,
|
|
||||||
FindByIDRequest,
|
|
||||||
FindListOptions,
|
|
||||||
FindListRequest,
|
|
||||||
GetRequest,
|
|
||||||
PatchRequest, PutRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
@@ -353,26 +329,24 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* Return an observable that emits response from the server
|
* Return an observable that emits response from the server
|
||||||
*/
|
*/
|
||||||
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
|
||||||
return hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href))
|
||||||
tap((href: string) => {
|
).subscribe((href: string) => {
|
||||||
this.requestService.removeByHrefSubstring(href);
|
const request = new FindListRequest(requestId, href, options);
|
||||||
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
|
||||||
if (hasValue(this.responseMsToLive)) {
|
if (hasValue(this.responseMsToLive)) {
|
||||||
request.responseMsToLive = this.responseMsToLive;
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
||||||
|
switchMap((requestEntry) =>
|
||||||
|
this.rdbService.buildList<T>(requestEntry.request.href, ...linksToFollow)
|
||||||
),
|
),
|
||||||
switchMap((href) => this.requestService.getByHref(href)),
|
|
||||||
skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
|
||||||
switchMap((href) =>
|
|
||||||
this.rdbService.buildList<T>(hrefObs, ...linksToFollow) as Observable<RemoteData<PaginatedList<T>>>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +365,9 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
map((href: string) => {
|
map((href: string) => {
|
||||||
const request = new PatchRequest(requestId, href, operations);
|
const request = new PatchRequest(requestId, href, operations);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
@@ -464,7 +441,13 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
|
|
||||||
const request$ = endpoint$.pipe(
|
const request$ = endpoint$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
|
map((endpoint: string) => {
|
||||||
|
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso));
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute the post request
|
// Execute the post request
|
||||||
@@ -513,7 +496,13 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
|
|
||||||
const request$ = endpoint$.pipe(
|
const request$ = endpoint$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
|
map((endpoint: string) => {
|
||||||
|
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso));
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute the post request
|
// Execute the post request
|
||||||
@@ -542,42 +531,9 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* @param dsoID The DSpace Object' id to be removed
|
* @param dsoID The DSpace Object' id to be removed
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||||
* metadata should be saved as real metadata
|
* metadata should be saved as real metadata
|
||||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
* @return the RestResponse as an Observable
|
||||||
*/
|
*/
|
||||||
delete(dsoID: string, copyVirtualMetadata?: string[]): Observable<boolean> {
|
delete(dsoID: string, copyVirtualMetadata?: string[]): Observable<RestResponse> {
|
||||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
|
||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
|
||||||
find((request: RequestEntry) => isNotEmpty(request) && request.completed),
|
|
||||||
map((request: RequestEntry) => request.response.isSuccessful)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing DSpace Object on the server
|
|
||||||
* @param dsoID The DSpace Object' id to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* Return an observable of the completed response
|
|
||||||
*/
|
|
||||||
deleteAndReturnResponse(dsoID: string, copyVirtualMetadata?: string[]): Observable<RestResponse> {
|
|
||||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
|
||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
|
||||||
hasValueOperator(),
|
|
||||||
find((request: RequestEntry) => request.completed),
|
|
||||||
map((request: RequestEntry) => request.response)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an existing DSpace Object on the server
|
|
||||||
* @param dsoID The DSpace Object' id to be removed
|
|
||||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
|
||||||
* metadata should be saved as real metadata
|
|
||||||
* Return the delete request's ID
|
|
||||||
*/
|
|
||||||
private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string {
|
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.getIDHrefObs(dsoID);
|
const hrefObs = this.getIDHrefObs(dsoID);
|
||||||
@@ -593,11 +549,17 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const request = new DeleteByIDRequest(requestId, href, dsoID);
|
const request = new DeleteByIDRequest(requestId, href, dsoID);
|
||||||
|
if (hasValue(this.responseMsToLive)) {
|
||||||
|
request.responseMsToLive = this.responseMsToLive;
|
||||||
|
}
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
})
|
})
|
||||||
).subscribe();
|
).subscribe();
|
||||||
|
|
||||||
return requestId;
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -608,4 +570,15 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
this.requestService.commit(method);
|
this.requestService.commit(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the links to traverse from the root of the api to the
|
||||||
|
* endpoint this DataService represents
|
||||||
|
*
|
||||||
|
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
||||||
|
* links to 'bar' the linkPath for the BarDataService would be
|
||||||
|
* 'foo/bar'
|
||||||
|
*/
|
||||||
|
getLinkPath(): string {
|
||||||
|
return this.linkPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,9 +16,10 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { CollectionDataService } from './collection-data.service';
|
import { CollectionDataService } from './collection-data.service';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap, map } from 'rxjs/operators';
|
||||||
import { BundleDataService } from './bundle-data.service';
|
import { BundleDataService } from './bundle-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
/**
|
/**
|
||||||
@@ -121,7 +122,7 @@ class DataServiceImpl extends ItemDataService {
|
|||||||
*/
|
*/
|
||||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||||
this.setRegularEndpoint();
|
this.setRegularEndpoint();
|
||||||
return super.delete(item.uuid);
|
return super.delete(item.uuid).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -63,6 +63,7 @@ export class LookupRelationService {
|
|||||||
concat(subject.pipe(take(1)))
|
concat(subject.pipe(take(1)))
|
||||||
)
|
)
|
||||||
) as any
|
) as any
|
||||||
|
,
|
||||||
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { filter, find, map, switchMap } from 'rxjs/operators';
|
import { filter, find, map, mergeMap, switchMap } from 'rxjs/operators';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { isNotUndefined } from '../../shared/empty.util';
|
import { isNotUndefined } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
@@ -76,7 +76,7 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
|||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
/* Flatten the page so we can treat it like an observable */
|
/* Flatten the page so we can treat it like an observable */
|
||||||
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
||||||
switchMap((type: RelationshipType) => {
|
mergeMap((type: RelationshipType) => {
|
||||||
if (type.leftwardType === label) {
|
if (type.leftwardType === label) {
|
||||||
return this.checkType(type, firstType, secondType);
|
return this.checkType(type, firstType, secondType);
|
||||||
} else if (type.rightwardType === label) {
|
} else if (type.rightwardType === label) {
|
||||||
@@ -92,7 +92,7 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
|||||||
// returns a void observable if there's not match
|
// returns a void observable if there's not match
|
||||||
// returns an observable that emits the relationship type when there is a match
|
// returns an observable that emits the relationship type when there is a match
|
||||||
private checkType(type: RelationshipType, firstType: string, secondType: string): Observable<RelationshipType> {
|
private checkType(type: RelationshipType, firstType: string, secondType: string): Observable<RelationshipType> {
|
||||||
const entityTypes = observableCombineLatest(type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData()));
|
const entityTypes = observableCombineLatest([type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData())]);
|
||||||
return entityTypes.pipe(
|
return entityTypes.pipe(
|
||||||
find(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType),
|
find(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType),
|
||||||
filter((types) => isNotUndefined(types)),
|
filter((types) => isNotUndefined(types)),
|
||||||
|
@@ -1,11 +1,6 @@
|
|||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
|
||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
import { spyOnOperator } from '../../shared/testing/utils.test';
|
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
@@ -13,11 +8,16 @@ import { Relationship } from '../shared/item-relationships/relationship.model';
|
|||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { RelationshipService } from './relationship.service';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { DeleteRequest, FindListOptions } from './request.models';
|
import { DeleteRequest, FindListOptions } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RelationshipService } from './relationship.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
|
import { spyOnOperator } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
describe('RelationshipService', () => {
|
describe('RelationshipService', () => {
|
||||||
let service: RelationshipService;
|
let service: RelationshipService;
|
||||||
@@ -159,8 +159,8 @@ describe('RelationshipService', () => {
|
|||||||
it('should clear the cache of the related items', () => {
|
it('should clear the cache of the related items', () => {
|
||||||
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1._links.self.href);
|
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1._links.self.href);
|
||||||
expect(objectCache.remove).toHaveBeenCalledWith(item._links.self.href);
|
expect(objectCache.remove).toHaveBeenCalledWith(item._links.self.href);
|
||||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid);
|
||||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,37 +175,6 @@ describe('RelationshipService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRelatedItems', () => {
|
|
||||||
let mockItem;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockItem = { uuid: 'someid' } as Item;
|
|
||||||
|
|
||||||
spyOn(service, 'getItemRelationshipsArray').and.returnValue(observableOf(relationships));
|
|
||||||
|
|
||||||
spyOnOperator(ItemRelationshipsUtils, 'relationsToItems').and.returnValue((v) => v);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call getItemRelationshipsArray with the correct params', (done) => {
|
|
||||||
service.getRelatedItems(mockItem).subscribe(() => {
|
|
||||||
expect(service.getItemRelationshipsArray).toHaveBeenCalledWith(
|
|
||||||
mockItem,
|
|
||||||
followLink('leftItem'),
|
|
||||||
followLink('rightItem'),
|
|
||||||
followLink('relationshipType')
|
|
||||||
);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the relationsToItems operator', (done) => {
|
|
||||||
service.getRelatedItems(mockItem).subscribe(() => {
|
|
||||||
expect(ItemRelationshipsUtils.relationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getRelatedItemsByLabel', () => {
|
describe('getRelatedItemsByLabel', () => {
|
||||||
let relationsList;
|
let relationsList;
|
||||||
let mockItem;
|
let mockItem;
|
||||||
@@ -258,7 +227,6 @@ describe('RelationshipService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||||
|
@@ -1,21 +1,14 @@
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import {
|
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
compareArraysUsingIds,
|
|
||||||
paginatedRelationsToItems,
|
|
||||||
relationsToItems
|
|
||||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
|
||||||
import { AppState, keySelector } from '../../app.reducer';
|
import { AppState, keySelector } from '../../app.reducer';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||||
import {
|
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||||
RemoveNameVariantAction,
|
|
||||||
SetNameVariantAction
|
|
||||||
} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
|
||||||
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
@@ -31,12 +24,7 @@ import { RelationshipType } from '../shared/item-relationships/relationship-type
|
|||||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type';
|
import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import {
|
import { configureRequest, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||||
configureRequest,
|
|
||||||
getRemoteDataPayload,
|
|
||||||
getResponseFromEntry,
|
|
||||||
getSucceededRemoteData
|
|
||||||
} from '../shared/operators';
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
@@ -55,6 +43,19 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele
|
|||||||
return keySelector<string>(itemID, relationshipListStateSelector(listID));
|
return keySelector<string>(itemID, relationshipListStateSelector(listID));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the Item in the payload of the source observable matches
|
||||||
|
* the given Item by UUID
|
||||||
|
*
|
||||||
|
* @param itemCheck the Item to compare with
|
||||||
|
*/
|
||||||
|
const compareItemsByUUID = (itemCheck: Item) =>
|
||||||
|
(source: Observable<RemoteData<Item>>): Observable<boolean> =>
|
||||||
|
source.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => item.uuid === itemCheck.uuid)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all relationship requests
|
* The service handling all relationship requests
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +63,7 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele
|
|||||||
@dataService(RELATIONSHIP)
|
@dataService(RELATIONSHIP)
|
||||||
export class RelationshipService extends DataService<Relationship> {
|
export class RelationshipService extends DataService<Relationship> {
|
||||||
protected linkPath = 'relationships';
|
protected linkPath = 'relationships';
|
||||||
|
protected responseMsToLive = 15 * 60 * 1000;
|
||||||
|
|
||||||
constructor(protected itemService: ItemDataService,
|
constructor(protected itemService: ItemDataService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
@@ -101,11 +103,7 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||||
getResponseFromEntry(),
|
getResponseFromEntry(),
|
||||||
switchMap((response) =>
|
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
|
||||||
this.clearRelatedCache(id).pipe(
|
|
||||||
map(() => response),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +130,8 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||||
getResponseFromEntry(),
|
getResponseFromEntry(),
|
||||||
tap(() => this.removeRelationshipItemsFromCache(item1)),
|
tap(() => this.refreshRelationshipItemsInCache(item1)),
|
||||||
tap(() => this.removeRelationshipItemsFromCache(item2))
|
tap(() => this.refreshRelationshipItemsInCache(item2))
|
||||||
) as Observable<RestResponse>;
|
) as Observable<RestResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,19 +139,19 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* Method to remove two items of a relationship from the cache using the identifier of the relationship
|
* Method to remove two items of a relationship from the cache using the identifier of the relationship
|
||||||
* @param relationshipId The identifier of the relationship
|
* @param relationshipId The identifier of the relationship
|
||||||
*/
|
*/
|
||||||
private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) {
|
private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) {
|
||||||
this.findById(relationshipId).pipe(
|
this.findById(relationshipId, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
switchMap((rel: Relationship) => combineLatest(
|
switchMap((rel: Relationship) => observableCombineLatest(
|
||||||
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
||||||
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe(([item1, item2]) => {
|
).subscribe(([item1, item2]) => {
|
||||||
this.removeRelationshipItemsFromCache(item1);
|
this.refreshRelationshipItemsInCache(item1);
|
||||||
this.removeRelationshipItemsFromCache(item2);
|
this.refreshRelationshipItemsInCache(item2);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +159,13 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* Method to remove an item that's part of a relationship from the cache
|
* Method to remove an item that's part of a relationship from the cache
|
||||||
* @param item The item to remove from the cache
|
* @param item The item to remove from the cache
|
||||||
*/
|
*/
|
||||||
private removeRelationshipItemsFromCache(item) {
|
public refreshRelationshipItemsInCache(item) {
|
||||||
this.objectCache.remove(item._links.self.href);
|
this.objectCache.remove(item._links.self.href);
|
||||||
this.requestService.removeByHrefSubstring(item.uuid);
|
this.requestService.removeByHrefSubstring(item.uuid);
|
||||||
combineLatest(
|
observableCombineLatest([
|
||||||
this.objectCache.hasBySelfLinkObservable(item._links.self.href),
|
this.objectCache.hasBySelfLinkObservable(item._links.self.href),
|
||||||
this.requestService.hasByHrefObservable(item.uuid)
|
this.requestService.hasByHrefObservable(item.self)
|
||||||
).pipe(
|
]).pipe(
|
||||||
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
||||||
take(1),
|
take(1),
|
||||||
switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1)))
|
switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1)))
|
||||||
@@ -176,7 +174,10 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an item's relationships in the form of an array
|
* Get an item's relationships in the form of an array
|
||||||
* @param item
|
*
|
||||||
|
* @param item The {@link Item} to get {@link Relationship}s for
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s
|
||||||
|
* should be automatically resolved
|
||||||
*/
|
*/
|
||||||
getItemRelationshipsArray(item: Item, ...linksToFollow: Array<FollowLinkConfig<Relationship>>): Observable<Relationship[]> {
|
getItemRelationshipsArray(item: Item, ...linksToFollow: Array<FollowLinkConfig<Relationship>>): Observable<Relationship[]> {
|
||||||
return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe(
|
return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe(
|
||||||
@@ -275,10 +276,10 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable<Relationship[]> {
|
getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable<Relationship[]> {
|
||||||
return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe(
|
return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||||
switchMap((relationships: Relationship[]) => {
|
switchMap((relationships: Relationship[]) => {
|
||||||
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
|
return observableCombineLatest(relationships.map((relationship: Relationship) => {
|
||||||
const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids);
|
const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids);
|
||||||
const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids);
|
const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids);
|
||||||
return observableCombineLatest(isLeftItem$, isRightItem$).pipe(
|
return observableCombineLatest([isLeftItem$, isRightItem$]).pipe(
|
||||||
filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
||||||
map(() => relationship),
|
map(() => relationship),
|
||||||
startWith(undefined)
|
startWith(undefined)
|
||||||
@@ -304,34 +305,31 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* @param label The rightward or leftward type of the relationship
|
* @param label The rightward or leftward type of the relationship
|
||||||
*/
|
*/
|
||||||
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable<Relationship> {
|
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable<Relationship> {
|
||||||
return this.getItemRelationshipsByLabel(item1, label, options, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem'))
|
return this.getItemRelationshipsByLabel(
|
||||||
.pipe(
|
item1,
|
||||||
getSucceededRemoteData(),
|
label,
|
||||||
isNotEmptyOperator(),
|
options,
|
||||||
map((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
|
followLink('relationshipType'),
|
||||||
mergeMap((relationships: Relationship[]) => {
|
followLink('leftItem'),
|
||||||
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
|
followLink('rightItem')
|
||||||
return observableCombineLatest(
|
|
||||||
this.isItemMatchWithItemRD(relationship.leftItem, item2),
|
|
||||||
this.isItemMatchWithItemRD(relationship.rightItem, item2)
|
|
||||||
).pipe(
|
).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
// the mergemap below will emit all elements of the list as separate events
|
||||||
|
mergeMap((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
|
||||||
|
mergeMap((relationship: Relationship) => {
|
||||||
|
return observableCombineLatest([
|
||||||
|
this.itemService.findByHref(relationship._links.leftItem.href).pipe(compareItemsByUUID(item2)),
|
||||||
|
this.itemService.findByHref(relationship._links.rightItem.href).pipe(compareItemsByUUID(item2))
|
||||||
|
]).pipe(
|
||||||
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
||||||
map((isMatch) => isMatch ? relationship : undefined)
|
map((isMatch) => isMatch ? relationship : undefined)
|
||||||
);
|
);
|
||||||
}))
|
|
||||||
}),
|
}),
|
||||||
map((relationships: Relationship[]) => relationships.find(((relationship) => hasValue(relationship))))
|
filter((relationship) => hasValue(relationship)),
|
||||||
|
take(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private isItemMatchWithItemRD(itemRD$: Observable<RemoteData<Item>>, itemCheck: Item): Observable<boolean> {
|
|
||||||
return itemRD$.pipe(
|
|
||||||
getSucceededRemoteData(),
|
|
||||||
map((itemRD: RemoteData<Item>) => itemRD.payload),
|
|
||||||
map((item: Item) => item.uuid === itemCheck.uuid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to set the name variant for specific list and item
|
* Method to set the name variant for specific list and item
|
||||||
* @param listID The list for which to save the name variant
|
* @param listID The list for which to save the name variant
|
||||||
@@ -378,7 +376,7 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
* @param nameVariant The name variant to set for the matching relationship
|
* @param nameVariant The name variant to set for the matching relationship
|
||||||
*/
|
*/
|
||||||
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
||||||
const update$: Observable<RemoteData<Relationship>> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((relation: Relationship) =>
|
switchMap((relation: Relationship) =>
|
||||||
relation.relationshipType.pipe(
|
relation.relationshipType.pipe(
|
||||||
@@ -400,16 +398,6 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
return this.update(updatedRelationship);
|
return this.update(updatedRelationship);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
update$.pipe(
|
|
||||||
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.RequestPending),
|
|
||||||
take(1),
|
|
||||||
).subscribe(() => {
|
|
||||||
this.removeRelationshipItemsFromCache(item1);
|
|
||||||
this.removeRelationshipItemsFromCache(item2);
|
|
||||||
});
|
|
||||||
|
|
||||||
return update$
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -432,7 +420,7 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
take(1),
|
take(1),
|
||||||
).subscribe((relationshipRD: RemoteData<Relationship>) => {
|
).subscribe((relationshipRD: RemoteData<Relationship>) => {
|
||||||
if (relationshipRD.state === RemoteDataState.ResponsePending) {
|
if (relationshipRD.state === RemoteDataState.ResponsePending) {
|
||||||
this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id);
|
this.refreshRelationshipItemsInCacheByRelationship(reoRel.relationship.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,18 +428,11 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear object and request caches of the items related to a relationship (left and right items)
|
* Patch isn't supported on the relationship endpoint, so use put instead.
|
||||||
* @param uuid The uuid of the relationship for which to clear the related items from the cache
|
*
|
||||||
|
* @param object the {@link Relationship} to update
|
||||||
*/
|
*/
|
||||||
clearRelatedCache(uuid: string): Observable<void> {
|
update(object: Relationship): Observable<RemoteData<Relationship>> {
|
||||||
return this.findById(uuid).pipe(
|
return this.put(object);
|
||||||
getSucceededRemoteData(),
|
|
||||||
map((rd: RemoteData<Relationship>) => {
|
|
||||||
this.objectCache.remove(rd.payload._links.leftItem.href);
|
|
||||||
this.objectCache.remove(rd.payload._links.rightItem.href);
|
|
||||||
this.requestService.removeByHrefSubstring(rd.payload._links.leftItem.href);
|
|
||||||
this.requestService.removeByHrefSubstring(rd.payload._links.rightItem.href);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -224,7 +224,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
* @param ePerson The EPerson to delete
|
* @param ePerson The EPerson to delete
|
||||||
*/
|
*/
|
||||||
public deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
public deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||||
return this.delete(ePerson.id);
|
return this.delete(ePerson.id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -135,7 +135,7 @@ export class GroupDataService extends DataService<Group> {
|
|||||||
* @param id The group id to delete
|
* @param id The group id to delete
|
||||||
*/
|
*/
|
||||||
public deleteGroup(group: Group): Observable<boolean> {
|
public deleteGroup(group: Group): Observable<boolean> {
|
||||||
return this.delete(group.id);
|
return this.delete(group.id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
|
||||||
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS, HttpRequest } from '@angular/common/http';
|
||||||
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
|
describe('ForwardClientIpInterceptor', () => {
|
||||||
|
let service: DSpaceRESTv2Service;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
let requestUrl;
|
||||||
|
let clientIp;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestUrl = 'test-url';
|
||||||
|
clientIp = '1.2.3.4';
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DSpaceRESTv2Service,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: ForwardClientIpInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{ provide: REQUEST, useValue: { get: () => undefined, connection: { remoteAddress: clientIp } }}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.get(DSpaceRESTv2Service);
|
||||||
|
httpMock = TestBed.get(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an X-Forwarded-For header matching the client\'s IP', () => {
|
||||||
|
service.get(requestUrl).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne(requestUrl);
|
||||||
|
expect(httpRequest.request.headers.get('X-Forwarded-For')).toEqual(clientIp);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
|
||||||
|
*/
|
||||||
|
export class ForwardClientIpInterceptor implements HttpInterceptor {
|
||||||
|
constructor(@Inject(REQUEST) protected req: any) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept http requests and add the client's IP to the X-Forwarded-For header
|
||||||
|
* @param httpRequest
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress;
|
||||||
|
return next.handle(httpRequest.clone({ setHeaders: { 'X-Forwarded-For': clientIp } }));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,9 @@
|
|||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CoreState } from '../../core.reducers';
|
import { CoreState } from '../../core.reducers';
|
||||||
import {
|
import { NewPatchAddOperationAction, NewPatchMoveOperationAction, NewPatchRemoveOperationAction, NewPatchReplaceOperationAction } from '../json-patch-operations.actions';
|
||||||
NewPatchAddOperationAction,
|
|
||||||
NewPatchRemoveOperationAction,
|
|
||||||
NewPatchReplaceOperationAction
|
|
||||||
} from '../json-patch-operations.actions';
|
|
||||||
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasNoValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { dateToISOFormat } from '../../../shared/date.util';
|
import { dateToISOFormat } from '../../../shared/date.util';
|
||||||
import { AuthorityValue } from '../../integration/models/authority.value';
|
import { AuthorityValue } from '../../integration/models/authority.value';
|
||||||
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
@@ -53,6 +49,9 @@ export class JsonPatchOperationsBuilder {
|
|||||||
* a boolean representing if the value to be added is a plain text value
|
* a boolean representing if the value to be added is a plain text value
|
||||||
*/
|
*/
|
||||||
replace(path: JsonPatchOperationPathObject, value, plain = false) {
|
replace(path: JsonPatchOperationPathObject, value, plain = false) {
|
||||||
|
if (hasNoValue(value) || (typeof value === 'object' && hasNoValue(value.value))) {
|
||||||
|
this.remove(path);
|
||||||
|
} else {
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
new NewPatchReplaceOperationAction(
|
new NewPatchReplaceOperationAction(
|
||||||
path.rootElement,
|
path.rootElement,
|
||||||
@@ -60,6 +59,26 @@ export class JsonPatchOperationsBuilder {
|
|||||||
path.path,
|
path.path,
|
||||||
this.prepareValue(value, plain, false)));
|
this.prepareValue(value, plain, false)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a new NewPatchMoveOperationAction
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* the new path tho move to
|
||||||
|
* @param prevPath
|
||||||
|
* the original path to move from
|
||||||
|
*/
|
||||||
|
move(path: JsonPatchOperationPathObject, prevPath: string) {
|
||||||
|
this.store.dispatch(
|
||||||
|
new NewPatchMoveOperationAction(
|
||||||
|
path.rootElement,
|
||||||
|
path.subRootElement,
|
||||||
|
prevPath,
|
||||||
|
path.path
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches a new NewPatchRemoveOperationAction
|
* Dispatches a new NewPatchRemoveOperationAction
|
||||||
|
@@ -196,7 +196,8 @@ function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperati
|
|||||||
body,
|
body,
|
||||||
action.type,
|
action.type,
|
||||||
action.payload.path,
|
action.payload.path,
|
||||||
hasValue(action.payload.value) ? action.payload.value : null);
|
hasValue(action.payload.value) ? action.payload.value : null,
|
||||||
|
hasValue(action.payload.from) ? action.payload.from : null);
|
||||||
|
|
||||||
if (hasValue(newState[ action.payload.resourceType ])
|
if (hasValue(newState[ action.payload.resourceType ])
|
||||||
&& hasValue(newState[ action.payload.resourceType ].children)) {
|
&& hasValue(newState[ action.payload.resourceType ].children)) {
|
||||||
@@ -293,7 +294,21 @@ function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOpera
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) {
|
/**
|
||||||
|
* Add a new operation to a patch
|
||||||
|
*
|
||||||
|
* @param body
|
||||||
|
* The current patch
|
||||||
|
* @param actionType
|
||||||
|
* The type of operation to add
|
||||||
|
* @param targetPath
|
||||||
|
* The path for the operation
|
||||||
|
* @param value
|
||||||
|
* The new value
|
||||||
|
* @param fromPath
|
||||||
|
* The previous path (in case of a move operation)
|
||||||
|
*/
|
||||||
|
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?, fromPath?) {
|
||||||
const newBody = Array.from(body);
|
const newBody = Array.from(body);
|
||||||
switch (actionType) {
|
switch (actionType) {
|
||||||
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
|
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
|
||||||
@@ -313,6 +328,9 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target
|
|||||||
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
|
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
|
||||||
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
|
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
|
||||||
break;
|
break;
|
||||||
|
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION:
|
||||||
|
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath }));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return newBody;
|
return newBody;
|
||||||
}
|
}
|
||||||
|
@@ -21,10 +21,8 @@ import {
|
|||||||
RollbacktPatchOperationsAction,
|
RollbacktPatchOperationsAction,
|
||||||
StartTransactionPatchOperationsAction
|
StartTransactionPatchOperationsAction
|
||||||
} from './json-patch-operations.actions';
|
} from './json-patch-operations.actions';
|
||||||
import { StoreMock } from '../../shared/testing/store.mock';
|
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import { storeModuleConfig } from '../../app.reducer';
|
|
||||||
|
|
||||||
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||||
protected linkPath = '';
|
protected linkPath = '';
|
||||||
@@ -99,27 +97,22 @@ describe('JsonPatchOperationsService test suite', () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async(() => {
|
function getStore() {
|
||||||
TestBed.configureTestingModule({
|
return jasmine.createSpyObj('store', {
|
||||||
imports: [
|
dispatch: {},
|
||||||
StoreModule.forRoot({}, storeModuleConfig),
|
select: observableOf(mockState['json/patch'][testJsonPatchResourceType]),
|
||||||
],
|
pipe: observableOf(true)
|
||||||
providers: [
|
});
|
||||||
{ provide: Store, useClass: StoreMock }
|
}
|
||||||
]
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = TestBed.get(Store);
|
store = getStore();
|
||||||
requestService = getMockRequestService(getRequestEntry$(true));
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
|
||||||
spyOn(store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType]));
|
|
||||||
spyOn(store, 'dispatch').and.callThrough();
|
|
||||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
});
|
});
|
||||||
@@ -164,7 +157,7 @@ describe('JsonPatchOperationsService test suite', () => {
|
|||||||
|
|
||||||
describe('when request is not successful', () => {
|
describe('when request is not successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = TestBed.get(Store);
|
store = getStore();
|
||||||
requestService = getMockRequestService(getRequestEntry$(false));
|
requestService = getMockRequestService(getRequestEntry$(false));
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
@@ -227,7 +220,7 @@ describe('JsonPatchOperationsService test suite', () => {
|
|||||||
|
|
||||||
describe('when request is not successful', () => {
|
describe('when request is not successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = TestBed.get(Store);
|
store = getStore();
|
||||||
requestService = getMockRequestService(getRequestEntry$(false));
|
requestService = getMockRequestService(getRequestEntry$(false));
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
|
74
src/app/core/locale/locale.interceptor.spec.ts
Normal file
74
src/app/core/locale/locale.interceptor.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing';
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { LocaleService } from './locale.service';
|
||||||
|
import { LocaleInterceptor } from './locale.interceptor';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
describe(`LocaleInterceptor`, () => {
|
||||||
|
let service: DSpaceRESTv2Service;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
let localeService: any;
|
||||||
|
|
||||||
|
const languageList = ['en;q=1', 'it;q=0.9', 'de;q=0.8', 'fr;q=0.7'];
|
||||||
|
|
||||||
|
const mockLocaleService = jasmine.createSpyObj('LocaleService', {
|
||||||
|
getCurrentLanguageCode: jasmine.createSpy('getCurrentLanguageCode'),
|
||||||
|
getLanguageCodeList: of(languageList)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [
|
||||||
|
DSpaceRESTv2Service,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: LocaleInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{provide: LocaleService, useValue: mockLocaleService},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.get(DSpaceRESTv2Service);
|
||||||
|
httpMock = TestBed.get(HttpTestingController);
|
||||||
|
localeService = TestBed.get(LocaleService);
|
||||||
|
|
||||||
|
localeService.getCurrentLanguageCode.and.returnValue('en')
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
|
||||||
|
it('should add an Accept-Language header when we’re sending an HTTP POST request', () => {
|
||||||
|
service.request(RestRequestMethod.POST, 'server/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne(`server/api/submission/workspaceitems`);
|
||||||
|
|
||||||
|
expect(httpRequest.request.headers.has('Accept-Language'));
|
||||||
|
const lang = httpRequest.request.headers.get('Accept-Language');
|
||||||
|
expect(lang).toBeDefined();
|
||||||
|
expect(lang).toBe(languageList.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an Accept-Language header when we’re sending an HTTP GET request', () => {
|
||||||
|
service.request(RestRequestMethod.GET, 'server/api/submission/workspaceitems/123').subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpRequest = httpMock.expectOne(`server/api/submission/workspaceitems/123`);
|
||||||
|
|
||||||
|
expect(httpRequest.request.headers.has('Accept-Language'));
|
||||||
|
const lang = httpRequest.request.headers.get('Accept-Language');
|
||||||
|
expect(lang).toBeDefined();
|
||||||
|
expect(lang).toBe(languageList.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
35
src/app/core/locale/locale.interceptor.ts
Normal file
35
src/app/core/locale/locale.interceptor.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { LocaleService } from './locale.service';
|
||||||
|
import { mergeMap, scan } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocaleInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(private localeService: LocaleService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept method
|
||||||
|
* @param req
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
let newReq: HttpRequest<any>;
|
||||||
|
return this.localeService.getLanguageCodeList()
|
||||||
|
.pipe(
|
||||||
|
scan((acc: any, value: any) => [...acc, ...value], []),
|
||||||
|
mergeMap((languages) => {
|
||||||
|
// Clone the request to add the new header.
|
||||||
|
newReq = req.clone({
|
||||||
|
headers: req.headers
|
||||||
|
.set('Accept-Language', languages.toString())
|
||||||
|
});
|
||||||
|
// Pass on the new request instead of the original request.
|
||||||
|
return next.handle(newReq);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
125
src/app/core/locale/locale.service.spec.ts
Normal file
125
src/app/core/locale/locale.service.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { LANG_COOKIE, LocaleService, LANG_ORIGIN } from './locale.service';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { AuthServiceMock } from 'src/app/shared/mocks/auth.service.mock';
|
||||||
|
import { NativeWindowRef } from '../services/window.service';
|
||||||
|
|
||||||
|
describe('LocaleService test suite', () => {
|
||||||
|
let service: LocaleService;
|
||||||
|
let serviceAsAny: any;
|
||||||
|
let cookieService: CookieService;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let window;
|
||||||
|
let spyOnGet;
|
||||||
|
let spyOnSet;
|
||||||
|
|
||||||
|
const langList = ['en', 'it', 'de'];
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
return TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||||
|
{ provide: AuthService, userValue: AuthServiceMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cookieService = TestBed.get(CookieService);
|
||||||
|
translateService = TestBed.get(TranslateService);
|
||||||
|
authService = TestBed.get(TranslateService);
|
||||||
|
window = new NativeWindowRef();
|
||||||
|
service = new LocaleService(window, cookieService, translateService, authService);
|
||||||
|
serviceAsAny = service;
|
||||||
|
spyOnGet = spyOn(cookieService, 'get');
|
||||||
|
spyOnSet = spyOn(cookieService, 'set');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCurrentLanguageCode', () => {
|
||||||
|
it('should return language saved on cookie', () => {
|
||||||
|
spyOnGet.and.returnValue('de');
|
||||||
|
expect(service.getCurrentLanguageCode()).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(translateService, 'getLangs').and.returnValue(langList);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return language from browser setting', () => {
|
||||||
|
spyOn(translateService, 'getBrowserLang').and.returnValue('it');
|
||||||
|
expect(service.getCurrentLanguageCode()).toBe('it');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default language from config', () => {
|
||||||
|
spyOn(translateService, 'getBrowserLang').and.returnValue('fr');
|
||||||
|
expect(service.getCurrentLanguageCode()).toBe('en');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLanguageCodeFromCookie', () => {
|
||||||
|
it('should return language from cookie', () => {
|
||||||
|
spyOnGet.and.returnValue('de');
|
||||||
|
expect(service.getLanguageCodeFromCookie()).toBe('de');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveLanguageCodeToCookie', () => {
|
||||||
|
it('should save language to cookie', () => {
|
||||||
|
service.saveLanguageCodeToCookie('en');
|
||||||
|
expect(spyOnSet).toHaveBeenCalledWith(LANG_COOKIE, 'en');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCurrentLanguageCode', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'saveLanguageCodeToCookie');
|
||||||
|
spyOn(translateService, 'use');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the given language', () => {
|
||||||
|
service.setCurrentLanguageCode('it');
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith( 'it');
|
||||||
|
expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('it');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the current language', () => {
|
||||||
|
spyOn(service, 'getCurrentLanguageCode').and.returnValue('es');
|
||||||
|
service.setCurrentLanguageCode();
|
||||||
|
expect(translateService.use).toHaveBeenCalledWith( 'es');
|
||||||
|
expect(service.saveLanguageCodeToCookie).toHaveBeenCalledWith('es');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
it('should set quality to current language list', () => {
|
||||||
|
const langListWithQuality = ['en;q=1', 'it;q=0.9', 'de;q=0.8'];
|
||||||
|
spyOn(service, 'setQuality').and.returnValue(langListWithQuality);
|
||||||
|
service.setQuality(langList, LANG_ORIGIN.BROWSER, false);
|
||||||
|
expect(service.setQuality).toHaveBeenCalledWith(langList, LANG_ORIGIN.BROWSER, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the list of language with quality factor', () => {
|
||||||
|
spyOn(service, 'getLanguageCodeList');
|
||||||
|
service.getLanguageCodeList();
|
||||||
|
expect(service.getLanguageCodeList).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
192
src/app/core/locale/locale.service.ts
Normal file
192
src/app/core/locale/locale.service.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { Injectable, Inject } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { Observable, of as observableOf, combineLatest } from 'rxjs';
|
||||||
|
import { map, take, flatMap } from 'rxjs/operators';
|
||||||
|
import { NativeWindowService, NativeWindowRef } from '../services/window.service';
|
||||||
|
|
||||||
|
export const LANG_COOKIE = 'language_cookie';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This enum defines the possible origin of the languages
|
||||||
|
*/
|
||||||
|
export enum LANG_ORIGIN {
|
||||||
|
UI,
|
||||||
|
EPERSON,
|
||||||
|
BROWSER
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to provide localization handler
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LocaleService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eperson language metadata
|
||||||
|
*/
|
||||||
|
EPERSON_LANG_METADATA = 'eperson.language';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
|
protected cookie: CookieService,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the language currently used
|
||||||
|
*
|
||||||
|
* @returns {string} The language code
|
||||||
|
*/
|
||||||
|
getCurrentLanguageCode(): string {
|
||||||
|
// Attempt to get the language from a cookie
|
||||||
|
let lang = this.getLanguageCodeFromCookie();
|
||||||
|
if (isEmpty(lang)) {
|
||||||
|
// Cookie not found
|
||||||
|
// Attempt to get the browser language from the user
|
||||||
|
if (this.translate.getLangs().includes(this.translate.getBrowserLang())) {
|
||||||
|
lang = this.translate.getBrowserLang();
|
||||||
|
} else {
|
||||||
|
lang = environment.defaultLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the languages list of the user in Accept-Language format
|
||||||
|
*
|
||||||
|
* @returns {Observable<string[]>}
|
||||||
|
*/
|
||||||
|
getLanguageCodeList(): Observable<string[]> {
|
||||||
|
const obs$ = combineLatest([
|
||||||
|
this.authService.isAuthenticated(),
|
||||||
|
this.authService.isAuthenticationLoaded()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return obs$.pipe(
|
||||||
|
take(1),
|
||||||
|
flatMap(([isAuthenticated, isLoaded]) => {
|
||||||
|
let epersonLang$: Observable<string[]> = observableOf([]);
|
||||||
|
if (isAuthenticated && isLoaded) {
|
||||||
|
epersonLang$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
map((eperson) => {
|
||||||
|
const languages: string[] = [];
|
||||||
|
const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA);
|
||||||
|
if (ePersonLang) {
|
||||||
|
languages.push(...this.setQuality(
|
||||||
|
[ePersonLang],
|
||||||
|
LANG_ORIGIN.EPERSON,
|
||||||
|
!isEmpty(this.translate.currentLang)));
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return epersonLang$.pipe(
|
||||||
|
map((epersonLang: string[]) => {
|
||||||
|
const languages: string[] = [];
|
||||||
|
if (this.translate.currentLang) {
|
||||||
|
languages.push(...this.setQuality(
|
||||||
|
[this.translate.currentLang],
|
||||||
|
LANG_ORIGIN.UI,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
if (isNotEmpty(epersonLang)) {
|
||||||
|
languages.push(...epersonLang);
|
||||||
|
}
|
||||||
|
if (navigator.languages) {
|
||||||
|
languages.push(...this.setQuality(
|
||||||
|
Object.assign([], navigator.languages),
|
||||||
|
LANG_ORIGIN.BROWSER,
|
||||||
|
!isEmpty(this.translate.currentLang))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the language from a cookie
|
||||||
|
*/
|
||||||
|
getLanguageCodeFromCookie(): string {
|
||||||
|
return this.cookie.get(LANG_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the language currently used
|
||||||
|
*
|
||||||
|
* @param lang
|
||||||
|
* The language to save
|
||||||
|
*/
|
||||||
|
saveLanguageCodeToCookie(lang: string): void {
|
||||||
|
this.cookie.set(LANG_COOKIE, lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the language currently used
|
||||||
|
*
|
||||||
|
* @param lang
|
||||||
|
* The language to set, if it's not provided retrieve default one
|
||||||
|
*/
|
||||||
|
setCurrentLanguageCode(lang?: string): void {
|
||||||
|
if (isEmpty(lang)) {
|
||||||
|
lang = this.getCurrentLanguageCode()
|
||||||
|
}
|
||||||
|
this.translate.use(lang);
|
||||||
|
this.saveLanguageCodeToCookie(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the quality factor for all element of input array.
|
||||||
|
* Returns a new array that contains the languages list with the quality value.
|
||||||
|
* The quality factor indicate the relative degree of preference for the language
|
||||||
|
* @param languages the languages list
|
||||||
|
* @param origin origin of language list (UI, EPERSON, BROWSER)
|
||||||
|
* @param hasOther true if contains other language, false otherwise
|
||||||
|
*/
|
||||||
|
setQuality(languages: string[], origin: LANG_ORIGIN, hasOther: boolean): string[] {
|
||||||
|
const langWithPrior = [];
|
||||||
|
let idx = 0;
|
||||||
|
const v = languages.length > 10 ? languages.length : 10;
|
||||||
|
let divisor: number;
|
||||||
|
switch (origin) {
|
||||||
|
case LANG_ORIGIN.EPERSON:
|
||||||
|
divisor = 2; break;
|
||||||
|
case LANG_ORIGIN.BROWSER:
|
||||||
|
divisor = (hasOther ? 10 : 1); break;
|
||||||
|
default:
|
||||||
|
divisor = 1;
|
||||||
|
}
|
||||||
|
languages.forEach( (lang) => {
|
||||||
|
let value = lang + ';q=';
|
||||||
|
let quality = (v - idx++) / v;
|
||||||
|
quality = ((languages.length > 10) ? quality.toFixed(2) : quality) as number;
|
||||||
|
value += quality / divisor;
|
||||||
|
langWithPrior.push(value);
|
||||||
|
});
|
||||||
|
return langWithPrior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh route navigated
|
||||||
|
*/
|
||||||
|
public refreshAfterChangeLanguage() {
|
||||||
|
// Hard redirect to the reload page with a unique number behind it
|
||||||
|
// so that all state is definitely lost
|
||||||
|
this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
src/app/core/locale/server-locale.service.ts
Normal file
60
src/app/core/locale/server-locale.service.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { LocaleService, LANG_ORIGIN } from './locale.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, combineLatest, of as observableOf } from 'rxjs';
|
||||||
|
import { take, flatMap, map } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty, isEmpty } from 'src/app/shared/empty.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerLocaleService extends LocaleService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the languages list of the user in Accept-Language format
|
||||||
|
*
|
||||||
|
* @returns {Observable<string[]>}
|
||||||
|
*/
|
||||||
|
getLanguageCodeList(): Observable<string[]> {
|
||||||
|
const obs$ = combineLatest([
|
||||||
|
this.authService.isAuthenticated(),
|
||||||
|
this.authService.isAuthenticationLoaded()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return obs$.pipe(
|
||||||
|
take(1),
|
||||||
|
flatMap(([isAuthenticated, isLoaded]) => {
|
||||||
|
let epersonLang$: Observable<string[]> = observableOf([]);
|
||||||
|
if (isAuthenticated && isLoaded) {
|
||||||
|
epersonLang$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
map((eperson) => {
|
||||||
|
const languages: string[] = [];
|
||||||
|
const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA);
|
||||||
|
if (ePersonLang) {
|
||||||
|
languages.push(...this.setQuality(
|
||||||
|
[ePersonLang],
|
||||||
|
LANG_ORIGIN.EPERSON,
|
||||||
|
!isEmpty(this.translate.currentLang)));
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return epersonLang$.pipe(
|
||||||
|
map((epersonLang: string[]) => {
|
||||||
|
const languages: string[] = [];
|
||||||
|
if (this.translate.currentLang) {
|
||||||
|
languages.push(...this.setQuality(
|
||||||
|
[this.translate.currentLang],
|
||||||
|
LANG_ORIGIN.UI,
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
if (isNotEmpty(epersonLang)) {
|
||||||
|
languages.push(...epersonLang);
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -127,7 +127,7 @@ describe('RegistryService', () => {
|
|||||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
|
||||||
findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
delete: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
clearRequests: observableOf('href')
|
clearRequests: observableOf('href')
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ describe('RegistryService', () => {
|
|||||||
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
create: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
create: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
put: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
put: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
delete: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
clearRequests: observableOf('href')
|
clearRequests: observableOf('href')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -223,7 +223,7 @@ export class RegistryService {
|
|||||||
* @param id The id of the metadata schema to delete
|
* @param id The id of the metadata schema to delete
|
||||||
*/
|
*/
|
||||||
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
||||||
return this.metadataSchemaService.deleteAndReturnResponse(`${id}`);
|
return this.metadataSchemaService.delete(`${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,7 +269,7 @@ export class RegistryService {
|
|||||||
* @param id The id of the metadata field to delete
|
* @param id The id of the metadata field to delete
|
||||||
*/
|
*/
|
||||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||||
return this.metadataFieldService.deleteAndReturnResponse(`${id}`);
|
return this.metadataFieldService.delete(`${id}`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Method that clears a cached metadata field request and returns its REST url
|
* Method that clears a cached metadata field request and returns its REST url
|
||||||
|
@@ -96,6 +96,8 @@ describe('ResourcePolicyService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
responseCacheEntry = new RequestEntry();
|
responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||||
|
responseCacheEntry.completed = true;
|
||||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
|
||||||
requestService = jasmine.createSpyObj('requestService', {
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
@@ -24,6 +24,8 @@ import { PaginatedList } from '../data/paginated-list';
|
|||||||
import { ActionType } from './models/action-type.model';
|
import { ActionType } from './models/action-type.model';
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ export class ResourcePolicyService {
|
|||||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
delete(resourcePolicyID: string): Observable<boolean> {
|
delete(resourcePolicyID: string): Observable<boolean> {
|
||||||
return this.dataService.delete(resourcePolicyID);
|
return this.dataService.delete(resourcePolicyID).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
9
src/app/core/shared/config-property.resource-type.ts
Normal file
9
src/app/core/shared/config-property.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for ConfigurationProperty
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const CONFIG_PROPERTY = new ResourceType('property');
|
48
src/app/core/shared/configuration-property.model.ts
Normal file
48
src/app/core/shared/configuration-property.model.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { autoserialize, autoserializeAs, deserialize } from 'cerialize';
|
||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
import { CONFIG_PROPERTY } from './config-property.resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model class for a Configuration Property
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class ConfigurationProperty implements CacheableObject {
|
||||||
|
static type = CONFIG_PROPERTY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The uuid of the configuration property
|
||||||
|
* The name is used as id for configuration properties
|
||||||
|
*/
|
||||||
|
@autoserializeAs(String, 'name')
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the configuration property
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The values of the configuration property
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
values: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links of the configuration property
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: { self: HALLink };
|
||||||
|
|
||||||
|
}
|
@@ -9,7 +9,8 @@ export enum Context {
|
|||||||
Workflow = 'workflow',
|
Workflow = 'workflow',
|
||||||
Workspace = 'workspace',
|
Workspace = 'workspace',
|
||||||
AdminMenu = 'adminMenu',
|
AdminMenu = 'adminMenu',
|
||||||
SubmissionModal = 'submissionModal',
|
EntitySearchModalWithNameVariants = 'EntitySearchModalWithNameVariants',
|
||||||
|
EntitySearchModal = 'EntitySearchModal',
|
||||||
AdminSearch = 'adminSearch',
|
AdminSearch = 'adminSearch',
|
||||||
AdminWorkflowSearch = 'adminWorkflowSearch',
|
AdminWorkflowSearch = 'adminWorkflowSearch',
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { autoserialize, Serialize, Deserialize } from 'cerialize';
|
|||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
export const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
||||||
|
|
||||||
/** A single metadata value and its properties. */
|
/** A single metadata value and its properties. */
|
||||||
export interface MetadataValueInterface {
|
export interface MetadataValueInterface {
|
||||||
|
@@ -66,7 +66,7 @@ export const getPaginatedListPayload = () =>
|
|||||||
|
|
||||||
export const getSucceededRemoteData = () =>
|
export const getSucceededRemoteData = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded), take(1));
|
||||||
|
|
||||||
export const getSucceededRemoteWithNotEmptyData = () =>
|
export const getSucceededRemoteWithNotEmptyData = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
|
@@ -7,12 +7,14 @@ import { SubmissionObjectDataService } from './submission-object-data.service';
|
|||||||
import { SubmissionScopeType } from './submission-scope-type';
|
import { SubmissionScopeType } from './submission-scope-type';
|
||||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
describe('SubmissionObjectDataService', () => {
|
describe('SubmissionObjectDataService', () => {
|
||||||
let service: SubmissionObjectDataService;
|
let service: SubmissionObjectDataService;
|
||||||
let submissionService: SubmissionService;
|
let submissionService: SubmissionService;
|
||||||
let workspaceitemDataService: WorkspaceitemDataService;
|
let workspaceitemDataService: WorkspaceitemDataService;
|
||||||
let workflowItemDataService: WorkflowItemDataService;
|
let workflowItemDataService: WorkflowItemDataService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
|
||||||
const submissionId = '1234';
|
const submissionId = '1234';
|
||||||
const wsiResult = 'wsiResult' as any;
|
const wsiResult = 'wsiResult' as any;
|
||||||
@@ -25,6 +27,9 @@ describe('SubmissionObjectDataService', () => {
|
|||||||
workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
|
workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
|
||||||
findById: wfiResult
|
findById: wfiResult
|
||||||
});
|
});
|
||||||
|
halService = jasmine.createSpyObj('HALEndpointService', {
|
||||||
|
getEndpoint: '/workspaceItem'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findById', () => {
|
describe('findById', () => {
|
||||||
@@ -32,7 +37,7 @@ describe('SubmissionObjectDataService', () => {
|
|||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||||
getSubmissionScope: {}
|
getSubmissionScope: {}
|
||||||
});
|
});
|
||||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||||
service.findById(submissionId);
|
service.findById(submissionId);
|
||||||
expect(submissionService.getSubmissionScope).toHaveBeenCalled();
|
expect(submissionService.getSubmissionScope).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -42,7 +47,7 @@ describe('SubmissionObjectDataService', () => {
|
|||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||||
getSubmissionScope: SubmissionScopeType.WorkspaceItem
|
getSubmissionScope: SubmissionScopeType.WorkspaceItem
|
||||||
});
|
});
|
||||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should forward the result of WorkspaceitemDataService.findByIdAndIDType()', () => {
|
it('should forward the result of WorkspaceitemDataService.findByIdAndIDType()', () => {
|
||||||
@@ -57,7 +62,7 @@ describe('SubmissionObjectDataService', () => {
|
|||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||||
getSubmissionScope: SubmissionScopeType.WorkflowItem
|
getSubmissionScope: SubmissionScopeType.WorkflowItem
|
||||||
});
|
});
|
||||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should forward the result of WorkflowItemDataService.findByIdAndIDType()', () => {
|
it('should forward the result of WorkflowItemDataService.findByIdAndIDType()', () => {
|
||||||
@@ -72,7 +77,7 @@ describe('SubmissionObjectDataService', () => {
|
|||||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||||
getSubmissionScope: 'Something else'
|
getSubmissionScope: 'Something else'
|
||||||
});
|
});
|
||||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldn\'t call any data service methods', () => {
|
it('shouldn\'t call any data service methods', () => {
|
||||||
|
@@ -8,6 +8,9 @@ import { SubmissionObject } from './models/submission-object.model';
|
|||||||
import { SubmissionScopeType } from './submission-scope-type';
|
import { SubmissionScopeType } from './submission-scope-type';
|
||||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve submission objects (WorkspaceItem/WorkflowItem)
|
* A service to retrieve submission objects (WorkspaceItem/WorkflowItem)
|
||||||
@@ -20,10 +23,22 @@ export class SubmissionObjectDataService {
|
|||||||
constructor(
|
constructor(
|
||||||
private workspaceitemDataService: WorkspaceitemDataService,
|
private workspaceitemDataService: WorkspaceitemDataService,
|
||||||
private workflowItemDataService: WorkflowItemDataService,
|
private workflowItemDataService: WorkflowItemDataService,
|
||||||
private submissionService: SubmissionService
|
private submissionService: SubmissionService,
|
||||||
|
private halService: HALEndpointService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the HREF for a specific object based on its identifier
|
||||||
|
* @param id The identifier for the object
|
||||||
|
*/
|
||||||
|
getHrefByID(id): Observable<string> {
|
||||||
|
const dataService: DataService<SubmissionObject> = this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem ? this.workspaceitemDataService : this.workflowItemDataService;
|
||||||
|
|
||||||
|
return this.halService.getEndpoint(dataService.getLinkPath()).pipe(
|
||||||
|
map((endpoint: string) => dataService.getIDHref(endpoint, encodeURIComponent(id))));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a submission object based on its ID.
|
* Retrieve a submission object based on its ID.
|
||||||
*
|
*
|
||||||
|
@@ -176,5 +176,4 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
|||||||
|
|
||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { find, map } from 'rxjs/operators';
|
import { find, map } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { RequestEntry } from '../data/request.reducer';
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make REST requests with workflow items endpoint.
|
* A service that provides methods to make REST requests with workflow items endpoint.
|
||||||
@@ -44,7 +45,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
* @param id The Workflow Item's id to be removed
|
* @param id The Workflow Item's id to be removed
|
||||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
delete(id: string): Observable<boolean> {
|
delete(id: string): Observable<RestResponse> {
|
||||||
return this.deleteWFI(id, true)
|
return this.deleteWFI(id, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
* @return an observable that emits true when sending back the item was successful, false when it failed
|
* @return an observable that emits true when sending back the item was successful, false when it failed
|
||||||
*/
|
*/
|
||||||
sendBack(id: string): Observable<boolean> {
|
sendBack(id: string): Observable<boolean> {
|
||||||
return this.deleteWFI(id, false)
|
return this.deleteWFI(id, false).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +65,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
* When true, the workflow item and its item will be permanently expunged on the server
|
* When true, the workflow item and its item will be permanently expunged on the server
|
||||||
* When false, the workflow item will be removed, but the item will still be available as a workspace item
|
* When false, the workflow item will be removed, but the item will still be available as a workspace item
|
||||||
*/
|
*/
|
||||||
private deleteWFI(id: string, expunge: boolean): Observable<boolean> {
|
private deleteWFI(id: string, expunge: boolean): Observable<RestResponse> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
@@ -82,7 +83,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
|||||||
|
|
||||||
return this.requestService.getByUUID(requestId).pipe(
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
find((request: RequestEntry) => request.completed),
|
find((request: RequestEntry) => request.completed),
|
||||||
map((request: RequestEntry) => request.response.isSuccessful)
|
map((request: RequestEntry) => request.response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,6 @@ import { WorkspaceItem } from './models/workspaceitem.model';
|
|||||||
@dataService(WorkspaceItem.type)
|
@dataService(WorkspaceItem.type)
|
||||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||||
protected linkPath = 'workspaceitems';
|
protected linkPath = 'workspaceitems';
|
||||||
protected responseMsToLive = 10 * 1000;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||||
|
20
src/app/curation-form/curation-form.component.html
Normal file
20
src/app/curation-form/curation-form.component.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="font-weight-bold" for="task">{{'curation.form.task-select.label' |translate }}</label>
|
||||||
|
<select id="task" formControlName="task" class="form-control">
|
||||||
|
<option *ngFor="let task of tasks" [value]="task">
|
||||||
|
{{ 'curation-task.task.' + task + '.label' | translate }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!hasHandleValue()" class="col-12 col-sm-6">
|
||||||
|
<label class="font-weight-bold" for="handle">{{'curation.form.handle.label' |translate }}</label>
|
||||||
|
<input id="handle" class="form-control" formControlName="handle">
|
||||||
|
<small class="text-muted">{{'curation.form.handle.hint' |translate }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-default btn-primary" type="submit">{{'curation.form.submit' |translate }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
165
src/app/curation-form/curation-form.component.spec.ts
Normal file
165
src/app/curation-form/curation-form.component.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { CurationFormComponent } from './curation-form.component';
|
||||||
|
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
||||||
|
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RequestEntry } from '../core/data/request.reducer';
|
||||||
|
import { DSOSuccessResponse, RestResponse } from '../core/cache/response.models';
|
||||||
|
import { Process } from '../process-page/processes/process.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
|
||||||
|
import { RouterStub } from '../shared/testing/router.stub';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||||
|
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||||
|
|
||||||
|
describe('CurationFormComponent', () => {
|
||||||
|
let comp: CurationFormComponent;
|
||||||
|
let fixture: ComponentFixture<CurationFormComponent>;
|
||||||
|
|
||||||
|
let scriptDataService: ScriptDataService;
|
||||||
|
let processDataService: ProcessDataService;
|
||||||
|
let configurationDataService: ConfigurationDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
let notificationsService;
|
||||||
|
let router;
|
||||||
|
|
||||||
|
const requestEntry = Object.assign(new RequestEntry(),
|
||||||
|
{response: new DSOSuccessResponse(['process-link'], 200, 'success')});
|
||||||
|
const failedRequestEntry = Object.assign(new RequestEntry(),
|
||||||
|
{response: new RestResponse(false, 400, 'Bad Request')});
|
||||||
|
|
||||||
|
const process = Object.assign(new Process(), {processId: 'process-id'});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
scriptDataService = jasmine.createSpyObj('scriptDataService', {
|
||||||
|
invoke: observableOf(requestEntry)
|
||||||
|
});
|
||||||
|
|
||||||
|
processDataService = jasmine.createSpyObj('processDataService', {
|
||||||
|
findByHref: createSuccessfulRemoteDataObject$(process)
|
||||||
|
});
|
||||||
|
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
getAuthenticatedUserFromStore: observableOf(Object.assign(new EPerson(), {email: 'test@mail'}))
|
||||||
|
});
|
||||||
|
|
||||||
|
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
|
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
|
name: 'plugin.named.org.dspace.curate.CurationTask',
|
||||||
|
values: [
|
||||||
|
'org.dspace.ctask.general.ProfileFormats = profileformats',
|
||||||
|
'',
|
||||||
|
'org.dspace.ctask.general.RequiredMetadata = requiredmetadata',
|
||||||
|
'org.dspace.ctask.general.MetadataValueLinkChecker = checklinks',
|
||||||
|
'value-to-be-skipped'
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
router = new RouterStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
|
||||||
|
declarations: [CurationFormComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ScriptDataService, useValue: scriptDataService},
|
||||||
|
{provide: ProcessDataService, useValue: processDataService},
|
||||||
|
{provide: AuthService, useValue: authService},
|
||||||
|
{provide: NotificationsService, useValue: notificationsService},
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CurationFormComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('init', () => {
|
||||||
|
it('should initialise the comp and contain the different tasks', () => {
|
||||||
|
expect(comp).toBeDefined();
|
||||||
|
|
||||||
|
const elements = fixture.debugElement.queryAll(By.css('option'));
|
||||||
|
expect(elements.length).toEqual(3);
|
||||||
|
expect(elements[0].nativeElement.innerHTML).toContain('curation-task.task.profileformats.label');
|
||||||
|
expect(elements[1].nativeElement.innerHTML).toContain('curation-task.task.requiredmetadata.label');
|
||||||
|
expect(elements[2].nativeElement.innerHTML).toContain('curation-task.task.checklinks.label');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('hasHandleValue', () => {
|
||||||
|
it('should return true when a dsoHandle value was provided', () => {
|
||||||
|
comp.dsoHandle = 'some-handle';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(comp.hasHandleValue()).toBeTrue();
|
||||||
|
});
|
||||||
|
it('should return false when no dsoHandle value was provided', () => {
|
||||||
|
expect(comp.hasHandleValue()).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('submit', () => {
|
||||||
|
it('should submit the selected process and handle to the scriptservice and navigate to the corresponding process page', () => {
|
||||||
|
comp.dsoHandle = 'test-handle';
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||||
|
{name: '-t', value: 'profileformats'},
|
||||||
|
{name: '-i', value: 'test-handle'},
|
||||||
|
{name: '-e', value: 'test@mail'},
|
||||||
|
], []);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(processDataService.findByHref).toHaveBeenCalledWith('process-link');
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/processes', 'process-id']);
|
||||||
|
});
|
||||||
|
it('should the selected process and handle to the scriptservice and stay on the page on error', () => {
|
||||||
|
(scriptDataService.invoke as jasmine.Spy).and.returnValue(observableOf(failedRequestEntry));
|
||||||
|
|
||||||
|
comp.dsoHandle = 'test-handle';
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||||
|
{name: '-t', value: 'profileformats'},
|
||||||
|
{name: '-i', value: 'test-handle'},
|
||||||
|
{name: '-e', value: 'test@mail'},
|
||||||
|
], []);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(processDataService.findByHref).not.toHaveBeenCalled();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should use the handle provided by the form when no dsoHandle is provided', () => {
|
||||||
|
comp.form.get('handle').patchValue('form-handle');
|
||||||
|
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||||
|
{name: '-t', value: 'profileformats'},
|
||||||
|
{name: '-i', value: 'form-handle'},
|
||||||
|
{name: '-e', value: 'test@mail'},
|
||||||
|
], []);
|
||||||
|
});
|
||||||
|
it('should use "all" when the handle provided by the form is empty and when no dsoHandle is provided', () => {
|
||||||
|
|
||||||
|
comp.submit();
|
||||||
|
|
||||||
|
expect(scriptDataService.invoke).toHaveBeenCalledWith('curate', [
|
||||||
|
{name: '-t', value: 'profileformats'},
|
||||||
|
{name: '-i', value: 'all'},
|
||||||
|
{name: '-e', value: 'test@mail'},
|
||||||
|
], []);
|
||||||
|
});
|
||||||
|
});
|
118
src/app/curation-form/curation-form.component.ts
Normal file
118
src/app/curation-form/curation-form.component.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { ScriptDataService } from '../core/data/processes/script-data.service';
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { getResponseFromEntry } from '../core/shared/operators';
|
||||||
|
import { DSOSuccessResponse } from '../core/cache/response.models';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||||
|
import { Process } from '../process-page/processes/process.model';
|
||||||
|
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||||
|
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { find } from 'rxjs/internal/operators/find';
|
||||||
|
|
||||||
|
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component responsible for rendering the Curation Task form
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-curation-form',
|
||||||
|
templateUrl: './curation-form.component.html'
|
||||||
|
})
|
||||||
|
export class CurationFormComponent implements OnInit {
|
||||||
|
|
||||||
|
config: Observable<RemoteData<ConfigurationProperty>>;
|
||||||
|
tasks: string[];
|
||||||
|
form: FormGroup;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
dsoHandle: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scriptDataService: ScriptDataService,
|
||||||
|
private configurationDataService: ConfigurationDataService,
|
||||||
|
private processDataService: ProcessDataService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
task: new FormControl(''),
|
||||||
|
handle: new FormControl('')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
|
||||||
|
this.config.pipe(
|
||||||
|
find((rd: RemoteData<ConfigurationProperty>) => rd.hasSucceeded),
|
||||||
|
map((rd: RemoteData<ConfigurationProperty>) => rd.payload)
|
||||||
|
).subscribe((configProperties) => {
|
||||||
|
this.tasks = configProperties.values
|
||||||
|
.filter((value) => isNotEmpty(value) && value.includes('='))
|
||||||
|
.map((value) => value.split('=')[1].trim());
|
||||||
|
this.form.get('task').patchValue(this.tasks[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the inputted dsoHandle has a value
|
||||||
|
*/
|
||||||
|
hasHandleValue() {
|
||||||
|
if (hasValue(this.dsoHandle)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the selected taskName and handle to the script data service to run the corresponding curation script
|
||||||
|
* Navigate to the process page on success
|
||||||
|
*/
|
||||||
|
submit() {
|
||||||
|
const taskName = this.form.get('task').value;
|
||||||
|
let handle;
|
||||||
|
if (this.hasHandleValue()) {
|
||||||
|
handle = this.dsoHandle;
|
||||||
|
} else {
|
||||||
|
handle = this.form.get('handle').value;
|
||||||
|
if (isEmpty(handle)) {
|
||||||
|
handle = 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((eperson: EPerson) => {
|
||||||
|
return this.scriptDataService.invoke('curate', [
|
||||||
|
{name: '-t', value: taskName},
|
||||||
|
{name: '-i', value: handle},
|
||||||
|
{name: '-e', value: eperson.email},
|
||||||
|
], []).pipe(getResponseFromEntry());
|
||||||
|
})
|
||||||
|
).subscribe((response: DSOSuccessResponse) => {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
|
||||||
|
this.translateService.get('curation.form.submit.success.content'));
|
||||||
|
this.processDataService.findByHref(response.resourceSelfLinks[0]).pipe(
|
||||||
|
filter((processRD: RemoteData<Process>) => hasValue(processRD) && hasValue(processRD.payload)),
|
||||||
|
take(1))
|
||||||
|
.subscribe((processRD: RemoteData<Process>) => {
|
||||||
|
this.router.navigate(['/processes', processRD.payload.processId]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
||||||
|
this.translateService.get('curation.form.submit.error.content'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +1 @@
|
|||||||
<ds-journal-issue-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-grid-element>
|
<ds-journal-issue-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-grid-element>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-journal-volume-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-grid-element>
|
<ds-journal-volume-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-grid-element>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-journal-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-grid-element>
|
<ds-journal-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-grid-element>
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-journal-issue-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-list-element>
|
<ds-journal-issue-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-list-element>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-journal-volume-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-list-element>
|
<ds-journal-volume-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-list-element>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-journal-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-list-element>
|
<ds-journal-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-list-element>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||||
<ds-truncatable [id]="dso.id">
|
<ds-truncatable [id]="dso.id">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-org-unit-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-grid-element>
|
<ds-org-unit-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-grid-element>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-person-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-grid-element>
|
<ds-person-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-grid-element>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user