mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into Item-page-redirects
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x]
|
node-version: [12.x, 14.x]
|
||||||
# Do NOT exit immediately if one matrix job fails
|
# Do NOT exit immediately if one matrix job fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# These are the actual CI steps to perform per job
|
# These are the actual CI steps to perform per job
|
||||||
|
@@ -13,7 +13,7 @@ You can find additional information on the DSpace 7 Angular UI on the [wiki](htt
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) `v10.x` or `v12.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
**Ensure you're running [Node](https://nodejs.org) `v12.x` or `v14.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -65,7 +65,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||||
- Ensure you're running node `v10.x` or `v12.x` and yarn >= `v1.x`
|
- Ensure you're running node `v12.x` or `v14.x` and yarn >= `v1.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
@@ -339,7 +339,6 @@ dspace-angular
|
|||||||
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
||||||
├── typedoc.json * TYPEDOC configuration
|
├── typedoc.json * TYPEDOC configuration
|
||||||
├── webpack * Webpack (https://webpack.github.io/) config directory
|
├── webpack * Webpack (https://webpack.github.io/) config directory
|
||||||
│ ├── helpers.js *
|
|
||||||
│ ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build
|
│ ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build
|
||||||
│ ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
|
│ ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
|
||||||
│ ├── webpack.common.js *
|
│ ├── webpack.common.js *
|
||||||
|
18
angular.json
18
angular.json
@@ -17,6 +17,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-builders/custom-webpack:browser",
|
"builder": "@angular-builders/custom-webpack:browser",
|
||||||
"options": {
|
"options": {
|
||||||
|
"extractCss": true,
|
||||||
"preserveSymlinks": true,
|
"preserveSymlinks": true,
|
||||||
"customWebpackConfig": {
|
"customWebpackConfig": {
|
||||||
"path": "./webpack/webpack.browser.ts",
|
"path": "./webpack/webpack.browser.ts",
|
||||||
@@ -46,7 +47,16 @@
|
|||||||
"src/robots.txt"
|
"src/robots.txt"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
{
|
||||||
|
"input": "src/styles/base-theme.scss",
|
||||||
|
"inject": false,
|
||||||
|
"bundleName": "base-theme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/themes/custom/styles/theme.scss",
|
||||||
|
"inject": false,
|
||||||
|
"bundleName": "custom-theme"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -116,7 +126,11 @@
|
|||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
{
|
||||||
|
"input": "src/styles/base-theme.scss",
|
||||||
|
"inject": false,
|
||||||
|
"bundleName": "base-theme"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
}
|
}
|
||||||
|
15
package.json
15
package.json
@@ -20,16 +20,17 @@
|
|||||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||||
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
|
"build:stats": "ng build --stats-json",
|
||||||
"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",
|
||||||
"ng-high-memory": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng",
|
"build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true",
|
||||||
"build:client-and-server-bundles": "npm run ng-high-memory -- build --prod && npm run ng-high-memory -- run dspace-angular:server:production --bundleDependencies true",
|
|
||||||
"test:watch": "npm-run-all --parallel config:test:watch test",
|
"test:watch": "npm-run-all --parallel config:test:watch test",
|
||||||
"test": "npm run ng-high-memory -- test --sourceMap=true --watch=true",
|
"test": "ng test --sourceMap=true --watch=true",
|
||||||
"test:headless": "npm run ng-high-memory -- test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint-fix": "npm run ng-high-memory -- lint --fix=true",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"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 --color",
|
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"codelyzer": "^6.0.1",
|
"codelyzer": "^6.0.1",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
"compression-webpack-plugin": "^3.0.1",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"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",
|
||||||
@@ -180,7 +181,7 @@
|
|||||||
"tslint": "^6.1.3",
|
"tslint": "^6.1.3",
|
||||||
"typescript": "~4.0.5",
|
"typescript": "~4.0.5",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-bundle-analyzer": "^3.3.2",
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-node-externals": "1.7.2"
|
"webpack-node-externals": "1.7.2"
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('postcss-import')(),
|
require('postcss-import')(),
|
||||||
require('postcss-cssnext')(),
|
require('postcss-preset-env')(),
|
||||||
require('postcss-apply')(),
|
require('postcss-apply')(),
|
||||||
require('postcss-responsive-type')()
|
require('postcss-responsive-type')()
|
||||||
]
|
]
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
const syncBuildDir = require('copyfiles');
|
|
||||||
const path = require('path');
|
|
||||||
const {
|
|
||||||
projectRoot,
|
|
||||||
theme,
|
|
||||||
themePath,
|
|
||||||
} = require('../webpack/helpers');
|
|
||||||
|
|
||||||
const projectDepth = projectRoot('./').split(path.sep).length;
|
|
||||||
|
|
||||||
let callback;
|
|
||||||
|
|
||||||
if (theme !== null && theme !== undefined) {
|
|
||||||
callback = () => {
|
|
||||||
syncBuildDir([path.join(themePath, '**/*'), 'build'], { up: projectDepth + 2 }, () => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
syncBuildDir([projectRoot('src/**/*'), 'build'], { up: projectDepth + 1 }, callback);
|
|
@@ -143,7 +143,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.route.params.subscribe((params) => {
|
this.subs.push(this.route.params.subscribe((params) => {
|
||||||
|
if (params.groupId !== 'newGroup') {
|
||||||
this.setActiveGroup(params.groupId);
|
this.setActiveGroup(params.groupId);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
@@ -225,14 +227,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
{
|
{
|
||||||
value: this.groupDescription.value
|
value: this.groupDescription.value
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (group === null) {
|
if (group === null) {
|
||||||
console.log('createNewGroup', values);
|
|
||||||
this.createNewGroup(values);
|
this.createNewGroup(values);
|
||||||
} else {
|
} else {
|
||||||
console.log('editGroup', group);
|
|
||||||
this.editGroup(group);
|
this.editGroup(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,10 +24,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(ePeopleSearchDtos | async)?.totalElements > 0"
|
||||||
[paginationOptions]="configSearch"
|
[paginationOptions]="configSearch"
|
||||||
[pageInfoState]="(searchResults$ | async)?.payload"
|
[pageInfoState]="(ePeopleSearchDtos | async)"
|
||||||
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
(pageChange)="onPageChangeSearch($event)">
|
(pageChange)="onPageChangeSearch($event)">
|
||||||
@@ -42,23 +42,23 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let ePerson of (searchResults$ | async)?.payload?.page">
|
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
||||||
<td>{{ePerson.id}}</td>
|
<td>{{ePerson.eperson.id}}</td>
|
||||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
|
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(isMemberOfGroup(ePerson) | async)"
|
<button *ngIf="(ePerson.memberOfGroup)"
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button *ngIf="!(isMemberOfGroup(ePerson) | async)"
|
<button *ngIf="!(ePerson.memberOfGroup)"
|
||||||
(click)="addMemberToGroup(ePerson)"
|
(click)="addMemberToGroup(ePerson)"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-plus fa-fw"></i>
|
<i class="fas fa-plus fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
<div *ngIf="(searchResults$ | async)?.payload?.totalElements == 0 && searchDone"
|
<div *ngIf="(ePeopleSearchDtos | async)?.totalElements == 0 && searchDone"
|
||||||
class="alert alert-info w-100 mb-2"
|
class="alert alert-info w-100 mb-2"
|
||||||
role="alert">
|
role="alert">
|
||||||
{{messagePrefix + '.no-items' | translate}}
|
{{messagePrefix + '.no-items' | translate}}
|
||||||
@@ -78,10 +78,10 @@
|
|||||||
|
|
||||||
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
||||||
|
|
||||||
<ds-pagination *ngIf="(members$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(ePeopleMembersOfGroupDtos | async)?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(members$ | async)?.payload"
|
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
|
||||||
[collectionSize]="(members$ | async)?.payload?.totalElements"
|
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
(pageChange)="onPageChange($event)">
|
(pageChange)="onPageChange($event)">
|
||||||
@@ -96,15 +96,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let ePerson of (members$ | async)?.payload?.page">
|
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
||||||
<td>{{ePerson.id}}</td>
|
<td>{{ePerson.eperson.id}}</td>
|
||||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
|
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
<button (click)="deleteMemberFromGroup(ePerson)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
<div *ngIf="(members$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
<div *ngIf="(ePeopleMembersOfGroupDtos | async) == undefined || (ePeopleMembersOfGroupDtos | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||||
role="alert">
|
role="alert">
|
||||||
{{messagePrefix + '.no-members-yet' | translate}}
|
{{messagePrefix + '.no-members-yet' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,9 +2,16 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
|
import {
|
||||||
import { map, mergeMap, take } from 'rxjs/operators';
|
Observable,
|
||||||
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
of as observableOf,
|
||||||
|
Subscription,
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
ObservedValueOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
||||||
@@ -13,18 +20,20 @@ import { Group } from '../../../../../core/eperson/models/group.model';
|
|||||||
import {
|
import {
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData
|
getFirstCompletedRemoteData,
|
||||||
|
getAllCompletedRemoteData
|
||||||
} from '../../../../../core/shared/operators';
|
} from '../../../../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { EpersonDtoModel } from '../../../../../core/eperson/models/eperson-dto.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys to keep track of specific subscriptions
|
* Keys to keep track of specific subscriptions
|
||||||
*/
|
*/
|
||||||
enum SubKey {
|
enum SubKey {
|
||||||
Members,
|
|
||||||
ActiveGroup,
|
ActiveGroup,
|
||||||
SearchResults,
|
MembersDTO,
|
||||||
|
SearchResultsDTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -42,11 +51,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* EPeople being displayed in search result, initially all members, after search result of search
|
* EPeople being displayed in search result, initially all members, after search result of search
|
||||||
*/
|
*/
|
||||||
searchResults$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
|
ePeopleSearchDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
||||||
/**
|
/**
|
||||||
* List of EPeople members of currently active group being edited
|
* List of EPeople members of currently active group being edited
|
||||||
*/
|
*/
|
||||||
members$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
|
ePeopleMembersOfGroupDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||||
@@ -130,15 +139,59 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private retrieveMembers(page: number) {
|
private retrieveMembers(page: number) {
|
||||||
this.unsubFrom(SubKey.Members);
|
this.unsubFrom(SubKey.MembersDTO);
|
||||||
this.subs.set(
|
this.subs.set(SubKey.MembersDTO, this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
||||||
SubKey.Members,
|
|
||||||
this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
|
}).pipe(
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<any>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
||||||
|
} else {
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
|
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
|
epersonDtoModel.eperson = member;
|
||||||
|
epersonDtoModel.memberOfGroup = isMember;
|
||||||
|
return epersonDtoModel;
|
||||||
|
});
|
||||||
|
return dto$;
|
||||||
|
}));
|
||||||
|
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
||||||
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
|
}));
|
||||||
|
}))
|
||||||
|
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||||
|
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the given ePerson is a member of the group currently being edited
|
||||||
|
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||||
|
*/
|
||||||
|
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||||
|
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||||
|
mergeMap((group: Group) => {
|
||||||
|
if (group != null) {
|
||||||
|
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 9999
|
||||||
|
}, false)
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
|
||||||
|
map((epeople: EPerson[]) => epeople.length > 0));
|
||||||
|
} else {
|
||||||
|
return observableOf(false);
|
||||||
}
|
}
|
||||||
).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
|
|
||||||
this.members$.next(rd);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +213,12 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* Deletes a given EPerson from the members list of the group currently being edited
|
* Deletes a given EPerson from the members list of the group currently being edited
|
||||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||||
*/
|
*/
|
||||||
deleteMemberFromGroup(ePerson: EPerson) {
|
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
this.showNotifications('deleteMember', response, ePerson.name, activeGroup);
|
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
||||||
|
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -175,40 +229,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* Adds a given EPerson to the members list of the group currently being edited
|
* Adds a given EPerson to the members list of the group currently being edited
|
||||||
* @param ePerson EPerson we want to add as member to group that is currently being edited
|
* @param ePerson EPerson we want to add as member to group that is currently being edited
|
||||||
*/
|
*/
|
||||||
addMemberToGroup(ePerson: EPerson) {
|
addMemberToGroup(ePerson: EpersonDtoModel) {
|
||||||
|
ePerson.memberOfGroup = true;
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson);
|
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
|
||||||
this.showNotifications('addMember', response, ePerson.name, activeGroup);
|
this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the given ePerson is a member of the group currently being edited
|
|
||||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
|
||||||
*/
|
|
||||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
|
||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
|
||||||
mergeMap((group: Group) => {
|
|
||||||
if (group != null) {
|
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
|
||||||
currentPage: 1,
|
|
||||||
elementsPerPage: 9999
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
|
|
||||||
map((epeople: EPerson[]) => epeople.length > 0));
|
|
||||||
} else {
|
|
||||||
return observableOf(false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search in the EPeople by name, email or metadata
|
* Search in the EPeople by name, email or metadata
|
||||||
* @param data Contains scope and query param
|
* @param data Contains scope and query param
|
||||||
@@ -228,12 +260,37 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.searchDone = true;
|
this.searchDone = true;
|
||||||
|
|
||||||
this.unsubFrom(SubKey.SearchResults);
|
this.unsubFrom(SubKey.SearchResultsDTO);
|
||||||
this.subs.set(SubKey.SearchResults, this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
this.subs.set(SubKey.SearchResultsDTO,
|
||||||
|
this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||||
currentPage: this.configSearch.currentPage,
|
currentPage: this.configSearch.currentPage,
|
||||||
elementsPerPage: this.configSearch.pageSize
|
elementsPerPage: this.configSearch.pageSize
|
||||||
}).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
|
}, false).pipe(
|
||||||
this.searchResults$.next(rd);
|
getAllCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<any>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
||||||
|
} else {
|
||||||
|
return rd;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
|
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
|
epersonDtoModel.eperson = member;
|
||||||
|
epersonDtoModel.memberOfGroup = isMember;
|
||||||
|
return epersonDtoModel;
|
||||||
|
});
|
||||||
|
return dto$;
|
||||||
|
}));
|
||||||
|
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
||||||
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
|
}));
|
||||||
|
}))
|
||||||
|
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||||
|
this.ePeopleSearchDtos.next(paginatedListOfDTOs);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="['newGroup']">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||||
<!-- <th scope="col">{{messagePrefix + 'table.comcol' | translate}}</th>-->
|
|
||||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -53,8 +52,7 @@
|
|||||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||||
<td>{{groupDto.group.id}}</td>
|
<td>{{groupDto.group.id}}</td>
|
||||||
<td>{{groupDto.group.name}}</td>
|
<td>{{groupDto.group.name}}</td>
|
||||||
<td>{{(getMembers(groupDto.group) | async)?.payload?.totalElements + (getSubgroups(groupDto.group) | async)?.payload?.totalElements}}</td>
|
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||||
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
|
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||||
@@ -63,7 +61,7 @@
|
|||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||||
(click)="deleteGroup(groupDto.group)" class="btn btn-outline-danger btn-sm"
|
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -26,7 +26,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|||||||
import {
|
import {
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getAllSucceededRemoteData
|
getFirstSucceededRemoteData
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
@@ -55,15 +55,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
currentPage: 1
|
currentPage: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of all the current Groups within the repository or the result of the search
|
|
||||||
*/
|
|
||||||
groups$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject<RemoteData<PaginatedList<Group>>>({} as any);
|
|
||||||
/**
|
/**
|
||||||
* A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or
|
* A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or
|
||||||
* as the result of the search
|
* as the result of the search
|
||||||
*/
|
*/
|
||||||
groupsDto$: BehaviorSubject<PaginatedList<GroupDtoModel>> = new BehaviorSubject<PaginatedList<GroupDtoModel>>({} as any);
|
groupsDto$: BehaviorSubject<PaginatedList<GroupDtoModel>> = new BehaviorSubject<PaginatedList<GroupDtoModel>>({} as any);
|
||||||
|
deletedGroupsIds: string[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable for the pageInfo, needed to pass to the pagination component
|
* An observable for the pageInfo, needed to pass to the pagination component
|
||||||
@@ -104,30 +101,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.search({ query: this.currentSearchQuery });
|
this.search({ query: this.currentSearchQuery });
|
||||||
|
|
||||||
this.subs.push(this.groups$.pipe(
|
|
||||||
getAllSucceededRemoteDataPayload(),
|
|
||||||
switchMap((groups: PaginatedList<Group>) => {
|
|
||||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
|
||||||
return observableCombineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
|
||||||
this.hasLinkedDSO(group)
|
|
||||||
]).pipe(
|
|
||||||
map(([isAuthorized, hasLinkedDSO]: boolean[]) => {
|
|
||||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
|
||||||
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
|
||||||
groupDtoModel.group = group;
|
|
||||||
return groupDtoModel;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
|
||||||
return buildPaginatedList(groups.pageInfo, dtos);
|
|
||||||
}));
|
|
||||||
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
|
||||||
this.groupsDto$.next(value);
|
|
||||||
this.pageInfoState$.next(value.pageInfo);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,14 +127,42 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.searchSub.unsubscribe();
|
this.searchSub.unsubscribe();
|
||||||
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||||
currentPage: this.config.currentPage,
|
currentPage: this.config.currentPage,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
}).pipe(
|
}).pipe(
|
||||||
getAllSucceededRemoteData()
|
getAllSucceededRemoteDataPayload(),
|
||||||
).subscribe((groupsRD: RemoteData<PaginatedList<Group>>) => {
|
switchMap((groups: PaginatedList<Group>) => {
|
||||||
this.groups$.next(groupsRD);
|
if (groups.page.length === 0) {
|
||||||
this.pageInfoState$.next(groupsRD.payload.pageInfo);
|
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||||
|
}
|
||||||
|
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||||
|
if (!this.deletedGroupsIds.includes(group.id)) {
|
||||||
|
return observableCombineLatest([
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||||
|
this.hasLinkedDSO(group),
|
||||||
|
this.getSubgroups(group),
|
||||||
|
this.getMembers(group)
|
||||||
|
]).pipe(
|
||||||
|
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
|
||||||
|
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
||||||
|
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||||
|
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
||||||
|
groupDtoModel.group = group;
|
||||||
|
groupDtoModel.subgroups = subgroups.payload;
|
||||||
|
groupDtoModel.epersons = members.payload;
|
||||||
|
return groupDtoModel;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||||
|
return buildPaginatedList(groups.pageInfo, dtos);
|
||||||
|
}));
|
||||||
|
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||||
|
this.groupsDto$.next(value);
|
||||||
|
this.pageInfoState$.next(value.pageInfo);
|
||||||
});
|
});
|
||||||
this.subs.push(this.searchSub);
|
this.subs.push(this.searchSub);
|
||||||
}
|
}
|
||||||
@@ -169,16 +170,17 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Delete Group
|
* Delete Group
|
||||||
*/
|
*/
|
||||||
deleteGroup(group: Group) {
|
deleteGroup(group: GroupDtoModel) {
|
||||||
if (hasValue(group.id)) {
|
if (hasValue(group.group.id)) {
|
||||||
this.groupService.delete(group.id).pipe(getFirstCompletedRemoteData())
|
this.groupService.delete(group.group.id).pipe(getFirstCompletedRemoteData())
|
||||||
.subscribe((rd: RemoteData<NoContent>) => {
|
.subscribe((rd: RemoteData<NoContent>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
|
||||||
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name }));
|
||||||
this.reset();
|
this.reset();
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(
|
this.notificationsService.error(
|
||||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }),
|
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }),
|
||||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage }));
|
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -201,7 +203,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href);
|
return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,7 +211,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.groupService.findAllByHref(group._links.subgroups.href);
|
return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,6 +220,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
map((rd: RemoteData<DSpaceObject>) => hasValue(rd) && hasValue(rd.payload)),
|
map((rd: RemoteData<DSpaceObject>) => hasValue(rd) && hasValue(rd.payload)),
|
||||||
catchError(() => observableOf(false)),
|
catchError(() => observableOf(false)),
|
||||||
);
|
);
|
||||||
@@ -233,15 +236,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.search({ query: '' });
|
this.search({ query: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract optional UUID from a group name => To be resolved to community or collection with link
|
|
||||||
* (Or will be resolved in backend and added to group object, tbd) //TODO
|
|
||||||
* @param groupName
|
|
||||||
*/
|
|
||||||
getOptionalComColFromName(groupName: string): string {
|
|
||||||
return this.groupService.getUUIDFromString(groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsub all subscriptions
|
* Unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
|
@@ -16,6 +16,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultGridElementComponent;
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
@@ -29,6 +31,8 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockThemeService = getMockThemeService();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
searchResult = new ItemSearchResult();
|
searchResult = new ItemSearchResult();
|
||||||
@@ -50,6 +54,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
|
{ provide: ThemeService, useValue: mockThemeService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -12,6 +12,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
|
|||||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -29,6 +30,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
|
|||||||
|
|
||||||
constructor(protected truncatableService: TruncatableService,
|
constructor(protected truncatableService: TruncatableService,
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
|
private themeService: ThemeService,
|
||||||
private componentFactoryResolver: ComponentFactoryResolver
|
private componentFactoryResolver: ComponentFactoryResolver
|
||||||
) {
|
) {
|
||||||
super(truncatableService, bitstreamDataService);
|
super(truncatableService, bitstreamDataService);
|
||||||
@@ -63,6 +65,6 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
|
|||||||
* @returns {GenericConstructor<Component>}
|
* @returns {GenericConstructor<Component>}
|
||||||
*/
|
*/
|
||||||
private getComponent(): GenericConstructor<Component> {
|
private getComponent(): GenericConstructor<Component> {
|
||||||
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined);
|
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
$icon-z-index: 10;
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
--ds-icon-z-index: 10;
|
||||||
|
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
nav {
|
nav {
|
||||||
background-color: $admin-sidebar-bg;
|
background-color: var(--ds-admin-sidebar-bg);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
> div {
|
> div {
|
||||||
@@ -19,12 +19,12 @@ $icon-z-index: 10;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.inactive ::ng-deep .sidebar-collapsible {
|
&.inactive ::ng-deep .sidebar-collapsible {
|
||||||
margin-left: -#{$sidebar-items-width};
|
margin-left: calc(-1 * var(--ds-sidebar-items-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-nav {
|
.navbar-nav {
|
||||||
.admin-menu-header {
|
.admin-menu-header {
|
||||||
background-color: $admin-sidebar-header-bg;
|
background-color: var(--ds-admin-sidebar-header-bg);
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
img {
|
img {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -43,29 +43,29 @@ $icon-z-index: 10;
|
|||||||
.sidebar-section {
|
.sidebar-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
background-color: $admin-sidebar-bg;
|
background-color: var(--ds-admin-sidebar-bg);
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding-top: $spacer;
|
padding-top: var(--bs-spacer);
|
||||||
padding-bottom: $spacer;
|
padding-bottom: var(--bs-spacer);
|
||||||
}
|
}
|
||||||
.shortcut-icon {
|
.shortcut-icon {
|
||||||
padding-left: $icon-padding;
|
padding-left: var(--ds-icon-padding);
|
||||||
padding-right: $icon-padding;
|
padding-right: var(--ds-icon-padding);
|
||||||
}
|
}
|
||||||
.shortcut-icon, .icon-wrapper {
|
.shortcut-icon, .icon-wrapper {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
z-index: $icon-z-index;
|
z-index: var(--ds-icon-z-index);
|
||||||
}
|
}
|
||||||
.sidebar-collapsible {
|
.sidebar-collapsible {
|
||||||
width: $sidebar-items-width;
|
width: var(--ds-sidebar-items-width);
|
||||||
position: relative;
|
position: relative;
|
||||||
a {
|
a {
|
||||||
padding-right: $spacer;
|
padding-right: var(--bs-spacer);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.active > .sidebar-collapsible > .nav-link {
|
&.active > .sidebar-collapsible > .nav-link {
|
||||||
color: $navbar-dark-active-color;
|
color: var(--bs-navbar-dark-active-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
:host ::ng-deep {
|
:host ::ng-deep {
|
||||||
.fa-chevron-right {
|
.fa-chevron-right {
|
||||||
padding-left: $spacer/2;
|
padding-left: calc(var(--bs-spacer) / 2);
|
||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
line-height: 3;
|
line-height: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sub-level-items {
|
.sidebar-sub-level-items {
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
color: $navbar-dark-color;
|
color: var(--bs-navbar-dark-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,8 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
@@ -28,6 +30,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
|||||||
let itemRD$;
|
let itemRD$;
|
||||||
let linkService;
|
let linkService;
|
||||||
let object;
|
let object;
|
||||||
|
let themeService;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||||
@@ -37,6 +40,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
|||||||
wfi.item = itemRD$;
|
wfi.item = itemRD$;
|
||||||
object.indexableObject = wfi;
|
object.indexableObject = wfi;
|
||||||
linkService = getMockLinkService();
|
linkService = getMockLinkService();
|
||||||
|
themeService = getMockThemeService();
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -51,6 +55,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
{
|
{
|
||||||
provide: TruncatableService, useValue: {
|
provide: TruncatableService, useValue: {
|
||||||
isCollapsed: () => observableOf(true),
|
isCollapsed: () => observableOf(true),
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import {
|
||||||
|
getListableObjectComponent,
|
||||||
|
listableObjectComponent
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
@@ -13,9 +16,13 @@ import { Observable } from 'rxjs';
|
|||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
import {
|
||||||
|
getAllSucceededRemoteData,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../../../../core/shared/operators';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -51,6 +58,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
|||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
protected truncatableService: TruncatableService,
|
protected truncatableService: TruncatableService,
|
||||||
|
private themeService: ThemeService,
|
||||||
protected bitstreamDataService: BitstreamDataService
|
protected bitstreamDataService: BitstreamDataService
|
||||||
) {
|
) {
|
||||||
super(truncatableService, bitstreamDataService);
|
super(truncatableService, bitstreamDataService);
|
||||||
@@ -92,7 +100,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
|||||||
* @returns {GenericConstructor<Component>}
|
* @returns {GenericConstructor<Component>}
|
||||||
*/
|
*/
|
||||||
private getComponent(item: Item): GenericConstructor<Component> {
|
private getComponent(item: Item): GenericConstructor<Component> {
|
||||||
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined);
|
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
::ng-deep {
|
::ng-deep {
|
||||||
.switch {
|
.switch {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $spacer*2.5;
|
top: calc(var(--bs-spacer) * 2.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,21 @@ describe('CollectionPageResolver', () => {
|
|||||||
describe('resolve', () => {
|
describe('resolve', () => {
|
||||||
let resolver: CollectionPageResolver;
|
let resolver: CollectionPageResolver;
|
||||||
let collectionService: any;
|
let collectionService: any;
|
||||||
|
let store: any;
|
||||||
const uuid = '1234-65487-12354-1235';
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
collectionService = {
|
collectionService = {
|
||||||
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
|
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
|
||||||
};
|
};
|
||||||
resolver = new CollectionPageResolver(collectionService);
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
});
|
||||||
|
resolver = new CollectionPageResolver(collectionService, store);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve a collection with the correct id', (done) => {
|
it('should resolve a collection with the correct id', (done) => {
|
||||||
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(resolved) => {
|
(resolved) => {
|
||||||
|
@@ -4,15 +4,31 @@ import { Collection } from '../core/shared/collection.model';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
|
||||||
|
followLink('parentCommunity', undefined, true, true, true,
|
||||||
|
followLink('parentCommunity')
|
||||||
|
),
|
||||||
|
followLink('logo')
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific collection before the route is activated
|
* This class represents a resolver that requests a specific collection before the route is activated
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
||||||
constructor(private collectionService: CollectionDataService) {
|
constructor(
|
||||||
|
private collectionService: CollectionDataService,
|
||||||
|
private store: Store<any>
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +39,19 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
|
|||||||
* or an error if something went wrong
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
|
||||||
return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe(
|
const collectionRD$ = this.collectionService.findById(
|
||||||
|
route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...COLLECTION_PAGE_LINKS_TO_FOLLOW
|
||||||
|
).pipe(
|
||||||
getFirstCompletedRemoteData()
|
getFirstCompletedRemoteData()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return collectionRD$;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,17 +6,21 @@ describe('CommunityPageResolver', () => {
|
|||||||
describe('resolve', () => {
|
describe('resolve', () => {
|
||||||
let resolver: CommunityPageResolver;
|
let resolver: CommunityPageResolver;
|
||||||
let communityService: any;
|
let communityService: any;
|
||||||
|
let store: any;
|
||||||
const uuid = '1234-65487-12354-1235';
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
communityService = {
|
communityService = {
|
||||||
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
|
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
|
||||||
};
|
};
|
||||||
resolver = new CommunityPageResolver(communityService);
|
store = jasmine.createSpyObj('store', {
|
||||||
|
dispatch: {},
|
||||||
|
});
|
||||||
|
resolver = new CommunityPageResolver(communityService, store);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve a community with the correct id', (done) => {
|
it('should resolve a community with the correct id', (done) => {
|
||||||
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(resolved) => {
|
(resolved) => {
|
||||||
|
@@ -4,15 +4,31 @@ import { Observable } from 'rxjs';
|
|||||||
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 { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { followLink } from '../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
|
export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Community>[] = [
|
||||||
|
followLink('logo'),
|
||||||
|
followLink('subcommunities'),
|
||||||
|
followLink('collections'),
|
||||||
|
followLink('parentCommunity')
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific community before the route is activated
|
* This class represents a resolver that requests a specific community before the route is activated
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
||||||
constructor(private communityService: CommunityDataService) {
|
constructor(
|
||||||
|
private communityService: CommunityDataService,
|
||||||
|
private store: Store<any>
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,15 +39,19 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
|
|||||||
* or an error if something went wrong
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
|
||||||
return this.communityService.findById(
|
const communityRD$ = this.communityService.findById(
|
||||||
route.params.id,
|
route.params.id,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
followLink('logo'),
|
...COMMUNITY_PAGE_LINKS_TO_FOLLOW
|
||||||
followLink('subcommunities'),
|
|
||||||
followLink('collections')
|
|
||||||
).pipe(
|
).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
communityRD$.subscribe((communityRD: RemoteData<Community>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, communityRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return communityRD$;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,11 +18,14 @@ import { PageInfo } from '../../core/shared/page-info.model';
|
|||||||
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 { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('CommunityPageSubCollectionList Component', () => {
|
describe('CommunityPageSubCollectionList Component', () => {
|
||||||
let comp: CommunityPageSubCollectionListComponent;
|
let comp: CommunityPageSubCollectionListComponent;
|
||||||
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
|
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
|
||||||
let collectionDataServiceStub: any;
|
let collectionDataServiceStub: any;
|
||||||
|
let themeService;
|
||||||
let subCollList = [];
|
let subCollList = [];
|
||||||
|
|
||||||
const collections = [Object.assign(new Community(), {
|
const collections = [Object.assign(new Community(), {
|
||||||
@@ -110,6 +113,8 @@ describe('CommunityPageSubCollectionList Component', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
themeService = getMockThemeService();
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -124,6 +129,7 @@ describe('CommunityPageSubCollectionList Component', () => {
|
|||||||
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -18,11 +18,14 @@ 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 { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('CommunityPageSubCommunityListComponent Component', () => {
|
describe('CommunityPageSubCommunityListComponent Component', () => {
|
||||||
let comp: CommunityPageSubCommunityListComponent;
|
let comp: CommunityPageSubCommunityListComponent;
|
||||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||||
let communityDataServiceStub: any;
|
let communityDataServiceStub: any;
|
||||||
|
let themeService;
|
||||||
let subCommList = [];
|
let subCommList = [];
|
||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
@@ -111,6 +114,8 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
themeService = getMockThemeService();
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -125,6 +130,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
|
|||||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: -$content-spacing;
|
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||||
margin-bottom: -$content-spacing;
|
margin-bottom: calc(-1 * var(--ds-content-spacing));
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-3 {
|
.display-3 {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-home-news',
|
selector: 'ds-home-news',
|
||||||
@@ -10,5 +10,4 @@ import { Component } from '@angular/core';
|
|||||||
* Component to render the news section on the home page
|
* Component to render the news section on the home page
|
||||||
*/
|
*/
|
||||||
export class HomeNewsComponent {
|
export class HomeNewsComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
27
src/app/+home-page/home-news/themed-home-news.component.ts
Normal file
27
src/app/+home-page/home-news/themed-home-news.component.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { HomeNewsComponent } from './home-news.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-home-news',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render the news section on the home page
|
||||||
|
*/
|
||||||
|
export class ThemedHomeNewsComponent extends ThemedComponent<HomeNewsComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'HomeNewsComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/+home-page/home-news/home-news.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./home-news.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,17 +1,17 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { HomePageComponent } from './home-page.component';
|
|
||||||
import { HomePageResolver } from './home-page.resolver';
|
import { HomePageResolver } from './home-page.resolver';
|
||||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||||
|
import { ThemedHomePageComponent } from './themed-home-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: HomePageComponent,
|
component: ThemedHomePageComponent,
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
data: {
|
data: {
|
||||||
title: 'home.title',
|
title: 'home.title',
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<ds-home-news></ds-home-news>
|
<ds-themed-home-news></ds-themed-home-news>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ng-container *ngIf="(site$ | async) as site">
|
<ng-container *ngIf="(site$ | async) as site">
|
||||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||||
|
@@ -7,6 +7,16 @@ import { HomePageRoutingModule } from './home-page-routing.module';
|
|||||||
import { HomePageComponent } from './home-page.component';
|
import { HomePageComponent } from './home-page.component';
|
||||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
|
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
|
||||||
|
import { ThemedHomePageComponent } from './themed-home-page.component';
|
||||||
|
|
||||||
|
const DECLARATIONS = [
|
||||||
|
HomePageComponent,
|
||||||
|
ThemedHomePageComponent,
|
||||||
|
TopLevelCommunityListComponent,
|
||||||
|
ThemedHomeNewsComponent,
|
||||||
|
HomeNewsComponent,
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,10 +26,11 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
StatisticsModule.forRoot()
|
StatisticsModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
HomePageComponent,
|
...DECLARATIONS,
|
||||||
TopLevelCommunityListComponent,
|
],
|
||||||
HomeNewsComponent,
|
exports: [
|
||||||
]
|
...DECLARATIONS,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class HomePageModule {
|
export class HomePageModule {
|
||||||
|
|
||||||
|
26
src/app/+home-page/themed-home-page.component.ts
Normal file
26
src/app/+home-page/themed-home-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||||
|
import { HomePageComponent } from './home-page.component';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-home-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedHomePageComponent extends ThemedComponent<HomePageComponent> {
|
||||||
|
protected inAndOutputNames: (keyof HomePageComponent & keyof this)[];
|
||||||
|
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'HomePageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../themes/${themeName}/app/+home-page/home-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./home-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -18,11 +18,14 @@ 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 { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('TopLevelCommunityList Component', () => {
|
describe('TopLevelCommunityList Component', () => {
|
||||||
let comp: TopLevelCommunityListComponent;
|
let comp: TopLevelCommunityListComponent;
|
||||||
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
|
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
|
||||||
let communityDataServiceStub: any;
|
let communityDataServiceStub: any;
|
||||||
|
let themeService;
|
||||||
|
|
||||||
const topCommList = [Object.assign(new Community(), {
|
const topCommList = [Object.assign(new Community(), {
|
||||||
id: '123456789-1',
|
id: '123456789-1',
|
||||||
@@ -101,6 +104,8 @@ describe('TopLevelCommunityList Component', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
themeService = getMockThemeService();
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -115,6 +120,7 @@ describe('TopLevelCommunityList Component', () => {
|
|||||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: SelectableListService, useValue: {} },
|
{ provide: SelectableListService, useValue: {} },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
.btn {
|
.btn {
|
||||||
min-width: $edit-item-button-min-width;
|
min-width: var(--ds-edit-item-button-min-width);
|
||||||
}
|
}
|
||||||
|
@@ -74,18 +74,18 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
|||||||
component: ItemRelationshipsComponent,
|
component: ItemRelationshipsComponent,
|
||||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||||
},
|
},
|
||||||
|
/* TODO - uncomment & fix when view page exists
|
||||||
{
|
{
|
||||||
path: 'view',
|
path: 'view',
|
||||||
/* TODO - change when view page exists */
|
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
|
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
|
||||||
},
|
}, */
|
||||||
|
/* TODO - uncomment & fix when curate page exists
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
/* TODO - change when curate page exists */
|
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
|
||||||
},
|
}, */
|
||||||
{
|
{
|
||||||
path: 'versionhistory',
|
path: 'versionhistory',
|
||||||
component: ItemVersionHistoryComponent,
|
component: ItemVersionHistoryComponent,
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
.header-row {
|
.header-row {
|
||||||
color: $table-dark-color;
|
color: var(--bs-table-dark-color);
|
||||||
background-color: $table-dark-bg;
|
background-color: var(--bs-table-dark-bg);
|
||||||
border-color: $table-dark-border-color;
|
border-color: var(--bs-table-dark-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bundle-row {
|
.bundle-row {
|
||||||
color: $table-head-color;
|
color: var(--bs-table-head-color);
|
||||||
background-color: $table-head-bg;
|
background-color: var(--bs-table-head-bg);
|
||||||
border-color: $table-border-color;
|
border-color: var(--bs-table-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-element {
|
.row-element {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
border-bottom: $table-border-width solid $table-border-color;
|
border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
.btn[disabled] {
|
.btn[disabled] {
|
||||||
color: $gray-600;
|
color: var(--bs-gray-600);
|
||||||
border-color: $gray-600;
|
border-color: var(--bs-gray-600);
|
||||||
z-index: 0; // prevent border colors jumping on hover
|
z-index: 0; // prevent border colors jumping on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-field {
|
.metadata-field {
|
||||||
width: $edit-item-metadata-field-width;
|
width: var(--ds-edit-item-metadata-field-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-field {
|
.language-field {
|
||||||
width: $edit-item-language-field-width;
|
width: var(--ds-edit-item-language-field-width);
|
||||||
}
|
}
|
@@ -1,19 +1,19 @@
|
|||||||
.button-row {
|
.button-row {
|
||||||
.btn {
|
.btn {
|
||||||
margin-right: 0.5 * $spacer;
|
margin-right: calc(0.5 * var(--bs-spacer));
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
|
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
|
||||||
min-width: $edit-item-button-min-width;
|
min-width: var(--ds-edit-item-button-min-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.top .btn {
|
&.top .btn {
|
||||||
margin-top: $spacer/2;
|
margin-top: calc(var(--bs-spacer) / 2);
|
||||||
margin-bottom: $spacer/2;
|
margin-bottom: calc(var(--bs-spacer) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
.relationship-row:not(.alert) {
|
.relationship-row:not(.alert) {
|
||||||
padding: $alert-padding-y 0;
|
padding: var(--bs-alert-padding-y) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationship-row.alert {
|
.relationship-row.alert {
|
||||||
margin-left: -$alert-padding-x;
|
margin-left: calc(-1 * var(--bs-alert-padding-x));
|
||||||
margin-right: -$alert-padding-x;
|
margin-right: calc(-1 * var(--bs-alert-padding-x));
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
.btn[disabled] {
|
.btn[disabled] {
|
||||||
color: $gray-600;
|
color: var(--bs-gray-600);
|
||||||
border-color: $gray-600;
|
border-color: var(--bs-gray-600);
|
||||||
z-index: 0; // prevent border colors jumping on hover
|
z-index: 0; // prevent border colors jumping on hover
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
.button-row {
|
.button-row {
|
||||||
.btn {
|
.btn {
|
||||||
margin-right: 0.5 * $spacer;
|
margin-right: calc(0.5 * var(--bs-spacer));
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
|
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
|
||||||
min-width: $edit-item-button-min-width;
|
min-width: var(--ds-edit-item-button-min-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.top .btn {
|
&.top .btn {
|
||||||
margin-top: $spacer/2;
|
margin-top: calc(var(--bs-spacer) / 2);
|
||||||
margin-bottom: $spacer/2;
|
margin-bottom: calc(var(--bs-spacer) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -32,17 +32,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
UntypedItemComponent
|
UntypedItemComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
const DECLARATIONS = [
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
SharedModule.withEntryComponents(),
|
|
||||||
ItemPageRoutingModule,
|
|
||||||
EditItemPageModule,
|
|
||||||
StatisticsModule.forRoot(),
|
|
||||||
JournalEntitiesModule.withEntryComponents(),
|
|
||||||
ResearchEntitiesModule.withEntryComponents()
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
ItemPageComponent,
|
ItemPageComponent,
|
||||||
FullItemPageComponent,
|
FullItemPageComponent,
|
||||||
MetadataUriValuesComponent,
|
MetadataUriValuesComponent,
|
||||||
@@ -60,6 +50,23 @@ const ENTRY_COMPONENTS = [
|
|||||||
ItemComponent,
|
ItemComponent,
|
||||||
UploadBitstreamComponent,
|
UploadBitstreamComponent,
|
||||||
AbstractIncrementalListComponent,
|
AbstractIncrementalListComponent,
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule.withEntryComponents(),
|
||||||
|
ItemPageRoutingModule,
|
||||||
|
EditItemPageModule,
|
||||||
|
StatisticsModule.forRoot(),
|
||||||
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
|
ResearchEntitiesModule.withEntryComponents()
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
...DECLARATIONS
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
...DECLARATIONS
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ItemPageModule {
|
export class ItemPageModule {
|
||||||
|
@@ -7,12 +7,21 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ResolvedAction } from '../core/resolving/resolver.actions';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { getItemPageRoute } from './item-page-routing-paths';
|
import { getItemPageRoute } from './item-page-routing-paths';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*/
|
||||||
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
||||||
followLink('owningCollection'),
|
followLink('owningCollection', undefined, true, true, true,
|
||||||
|
followLink('parentCommunity', undefined, true, true, true,
|
||||||
|
followLink('parentCommunity'))
|
||||||
|
),
|
||||||
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
|
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
|
||||||
followLink('relationships'),
|
followLink('relationships'),
|
||||||
followLink('version', undefined, true, true, true, followLink('versionhistory')),
|
followLink('version', undefined, true, true, true, followLink('versionhistory')),
|
||||||
@@ -23,8 +32,11 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
||||||
constructor(private itemService: ItemDataService,
|
constructor(
|
||||||
private router: Router) {
|
private itemService: ItemDataService,
|
||||||
|
private store: Store<any>,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +47,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
* or an error if something went wrong
|
* or an error if something went wrong
|
||||||
*/
|
*/
|
||||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
return this.itemService.findById(route.params.id,
|
const itemRD$ = this.itemService.findById(route.params.id,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
...ITEM_PAGE_LINKS_TO_FOLLOW
|
...ITEM_PAGE_LINKS_TO_FOLLOW
|
||||||
@@ -54,5 +66,11 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
|
|||||||
return rd;
|
return rd;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return itemRD$;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
.login-logo {
|
.login-logo {
|
||||||
height: $login-logo-height;
|
height: var(--ds-login-logo-height);
|
||||||
width: $login-logo-width;
|
width: var(--ds-login-logo-width);
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,10 @@ import { UploaderService } from '../../shared/uploader/uploader.service';
|
|||||||
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 { UploaderComponent } from '../../shared/uploader/uploader.component';
|
import { UploaderComponent } from '../../shared/uploader/uploader.component';
|
||||||
|
import { HttpXsrfTokenExtractor } from '@angular/common/http';
|
||||||
|
import { CookieService } from '../../core/services/cookie.service';
|
||||||
|
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
||||||
|
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
|
||||||
|
|
||||||
describe('MyDSpaceNewSubmissionComponent test', () => {
|
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||||
|
|
||||||
@@ -55,6 +59,8 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
MyDSpaceNewSubmissionComponent,
|
MyDSpaceNewSubmissionComponent,
|
||||||
UploaderService,
|
UploaderService,
|
||||||
|
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
|
||||||
|
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
[configurationList]="(configurationList$ | async)"
|
[configurationList]="(configurationList$ | async)"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
[viewModeList]="viewModeList"
|
[viewModeList]="viewModeList"
|
||||||
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-9">
|
||||||
<ds-search-form id="search-form"
|
<ds-search-form id="search-form"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()"
|
(toggleSidebar)="closeSidebar()"
|
||||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||||
|
[refreshFilters]="refreshFilters.asObservable()"
|
||||||
[inPlaceSearch]="inPlaceSearch">
|
[inPlaceSearch]="inPlaceSearch">
|
||||||
</ds-search-sidebar>
|
</ds-search-sidebar>
|
||||||
<div id="search-content" class="col-12">
|
<div id="search-content" class="col-12">
|
||||||
@@ -39,7 +41,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<ds-my-dspace-results [searchResults]="resultsRD$ | async"
|
<ds-my-dspace-results [searchResults]="resultsRD$ | async"
|
||||||
[searchConfig]="searchOptions$ | async"
|
[searchConfig]="searchOptions$ | async"
|
||||||
[context]="context$ | async"></ds-my-dspace-results>
|
[context]="context$ | async"
|
||||||
|
(contentChange)="onResultsContentChange()"></ds-my-dspace-results>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,7 +7,7 @@ import {
|
|||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { map, switchMap, tap, } from 'rxjs/operators';
|
import { map, switchMap, tap, } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
@@ -101,6 +101,11 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
context$: Observable<Context>;
|
context$: Observable<Context>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event every time search sidebars must refresh their contents.
|
||||||
|
*/
|
||||||
|
refreshFilters: Subject<any> = new Subject<any>();
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
private sidebarService: SidebarService,
|
private sidebarService: SidebarService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
@@ -148,6 +153,14 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the contentChange event from within the my dspace content.
|
||||||
|
* Notify search sidebars to refresh their content.
|
||||||
|
*/
|
||||||
|
onResultsContentChange() {
|
||||||
|
this.refreshFilters.next();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the sidebar to a collapsed state
|
* Set the sidebar to a collapsed state
|
||||||
*/
|
*/
|
||||||
@@ -184,5 +197,6 @@ export class MyDSpacePageComponent implements OnInit {
|
|||||||
if (hasValue(this.sub)) {
|
if (hasValue(this.sub)) {
|
||||||
this.sub.unsubscribe();
|
this.sub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
this.refreshFilters.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,8 @@
|
|||||||
[sortConfig]="searchConfig.sort"
|
[sortConfig]="searchConfig.sort"
|
||||||
[objects]="searchResults"
|
[objects]="searchResults"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[context]="context">
|
[context]="context"
|
||||||
|
(contentChange)="contentChange.emit()">
|
||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
|
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
@@ -41,6 +41,12 @@ export class MyDSpaceResultsComponent {
|
|||||||
* The current context for the search results
|
* The current context for the search results
|
||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit when one of the results has changed.
|
||||||
|
*/
|
||||||
|
@Output() contentChange = new EventEmitter<any>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if search results entry are separated by a line
|
* A boolean representing if search results entry are separated by a line
|
||||||
*/
|
*/
|
||||||
|
@@ -14,12 +14,16 @@ import { ClaimedTaskSearchResultDetailElementComponent } from '../shared/object-
|
|||||||
import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component';
|
import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component';
|
||||||
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
|
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
|
||||||
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
|
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
|
||||||
|
import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component';
|
||||||
|
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
WorkspaceItemSearchResultListElementComponent,
|
WorkspaceItemSearchResultListElementComponent,
|
||||||
WorkflowItemSearchResultListElementComponent,
|
WorkflowItemSearchResultListElementComponent,
|
||||||
ClaimedSearchResultListElementComponent,
|
ClaimedSearchResultListElementComponent,
|
||||||
|
ClaimedApprovedSearchResultListElementComponent,
|
||||||
|
ClaimedDeclinedSearchResultListElementComponent,
|
||||||
PoolSearchResultListElementComponent,
|
PoolSearchResultListElementComponent,
|
||||||
ItemSearchResultDetailElementComponent,
|
ItemSearchResultDetailElementComponent,
|
||||||
WorkspaceItemSearchResultDetailElementComponent,
|
WorkspaceItemSearchResultDetailElementComponent,
|
||||||
|
@@ -2,20 +2,63 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
|
import { Component, ViewChild } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RouteService } from '../core/services/route.service';
|
||||||
|
import createSpy = jasmine.createSpy;
|
||||||
|
|
||||||
|
const CONFIGURATION = 'test-configuration';
|
||||||
|
const QUERY = 'test query';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<ds-configuration-search-page [configuration]="'${CONFIGURATION}'"
|
||||||
|
[fixedFilterQuery]="'${QUERY}'"
|
||||||
|
#configurationSearchPage>
|
||||||
|
</ds-configuration-search-page>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class HostComponent {
|
||||||
|
@ViewChild('configurationSearchPage') configurationSearchPage: ConfigurationSearchPageComponent;
|
||||||
|
}
|
||||||
|
|
||||||
describe('ConfigurationSearchPageComponent', () => {
|
describe('ConfigurationSearchPageComponent', () => {
|
||||||
let comp: ConfigurationSearchPageComponent;
|
let comp: ConfigurationSearchPageComponent;
|
||||||
let fixture: ComponentFixture<ConfigurationSearchPageComponent>;
|
let fixture: ComponentFixture<HostComponent>;
|
||||||
let searchConfigService: SearchConfigurationService;
|
let searchConfigService: SearchConfigurationService;
|
||||||
|
let routeService: RouteService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
configureSearchComponentTestingModule(ConfigurationSearchPageComponent);
|
configureSearchComponentTestingModule(ConfigurationSearchPageComponent, [HostComponent]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ConfigurationSearchPageComponent);
|
fixture = TestBed.createComponent(HostComponent);
|
||||||
comp = fixture.componentInstance;
|
|
||||||
searchConfigService = (comp as any).searchConfigService;
|
// Set router url to a dummy value for SearchComponent#ngOnInit
|
||||||
|
spyOnProperty(TestBed.inject(Router), 'url', 'get').and.returnValue('some/url/here');
|
||||||
|
|
||||||
|
routeService = TestBed.inject(RouteService);
|
||||||
|
routeService.setParameter = createSpy('setParameter');
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
comp = fixture.componentInstance.configurationSearchPage;
|
||||||
|
searchConfigService = (comp as any).searchConfigService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set route parameters on init', () => {
|
||||||
|
expect(comp.configuration).toBe(CONFIGURATION);
|
||||||
|
expect(comp.fixedFilterQuery).toBe(QUERY);
|
||||||
|
|
||||||
|
expect(routeService.setParameter).toHaveBeenCalledWith('configuration', CONFIGURATION);
|
||||||
|
expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', QUERY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset route parameters on destroy', () => {
|
||||||
|
fixture.destroy();
|
||||||
|
|
||||||
|
expect(routeService.setParameter).toHaveBeenCalledWith('configuration', undefined);
|
||||||
|
expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,14 @@
|
|||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchComponent } from './search.component';
|
||||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||||
@@ -27,7 +34,7 @@ import { Router } from '@angular/router';
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
|
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* The configuration to use for the search options
|
* The configuration to use for the search options
|
||||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||||
@@ -65,4 +72,17 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
|
|||||||
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the updated query/configuration set in ngOnInit()
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
if (hasValue(this.configuration)) {
|
||||||
|
this.routeService.setParameter('configuration', undefined);
|
||||||
|
}
|
||||||
|
if (hasValue(this.fixedFilterQuery)) {
|
||||||
|
this.routeService.setParameter('fixedFilterQuery', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .search-controls {
|
::ng-deep .search-controls {
|
||||||
margin-bottom: $spacer;
|
margin-bottom: var(--bs-spacer);
|
||||||
}
|
}
|
||||||
|
@@ -84,10 +84,10 @@ const routeServiceStub = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function configureSearchComponentTestingModule(compType) {
|
export function configureSearchComponentTestingModule(compType, additionalDeclarations: any[] = []) {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule],
|
||||||
declarations: [compType],
|
declarations: [compType, ...additionalDeclarations],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{
|
{
|
||||||
|
@@ -5,6 +5,7 @@ import { Item } from './core/shared/item.model';
|
|||||||
import { getCommunityPageRoute } from './+community-page/community-page-routing-paths';
|
import { getCommunityPageRoute } from './+community-page/community-page-routing-paths';
|
||||||
import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths';
|
import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths';
|
||||||
import { getItemPageRoute } from './+item-page/item-page-routing-paths';
|
import { getItemPageRoute } from './+item-page/item-page-routing-paths';
|
||||||
|
import { hasValue } from './shared/empty.util';
|
||||||
|
|
||||||
export const BITSTREAM_MODULE_PATH = 'bitstreams';
|
export const BITSTREAM_MODULE_PATH = 'bitstreams';
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export function getWorkflowItemModuleRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDSORoute(dso: DSpaceObject): string {
|
export function getDSORoute(dso: DSpaceObject): string {
|
||||||
|
if (hasValue(dso)) {
|
||||||
switch ((dso as any).type) {
|
switch ((dso as any).type) {
|
||||||
case Community.type.value:
|
case Community.type.value:
|
||||||
return getCommunityPageRoute(dso.uuid);
|
return getCommunityPageRoute(dso.uuid);
|
||||||
@@ -53,6 +55,7 @@ export function getDSORoute(dso: DSpaceObject): string {
|
|||||||
case Item.type.value:
|
case Item.type.value:
|
||||||
return getItemPageRoute(dso as Item);
|
return getItemPageRoute(dso as Item);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FORBIDDEN_PATH = '403';
|
export const FORBIDDEN_PATH = '403';
|
||||||
|
@@ -1,30 +1 @@
|
|||||||
<div class="outer-wrapper" *ngIf="isNotAuthBlocking$ | async; else authLoader">
|
<ds-themed-root [isNotAuthBlocking]="isNotAuthBlocking$ | async" [isLoading]="isLoading$ | async"></ds-themed-root>
|
||||||
<ds-admin-sidebar></ds-admin-sidebar>
|
|
||||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
|
||||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
|
||||||
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
|
|
||||||
}">
|
|
||||||
<ds-header-navbar-wrapper></ds-header-navbar-wrapper>
|
|
||||||
|
|
||||||
<ds-notifications-board
|
|
||||||
[options]="notificationOptions">
|
|
||||||
</ds-notifications-board>
|
|
||||||
<main class="main-content">
|
|
||||||
<div class="container">
|
|
||||||
<ds-breadcrumbs></ds-breadcrumbs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" *ngIf="isLoading$ | async">
|
|
||||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
|
||||||
</div>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<ds-footer></ds-footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-template #authLoader>
|
|
||||||
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center">
|
|
||||||
<ds-loading [showMessage]="false"></ds-loading>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
@import '../styles/helpers/font_awesome_imports.scss';
|
|
||||||
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
|
||||||
@import '../../node_modules/nouislider/distribute/nouislider.min';
|
|
||||||
|
|
||||||
html {
|
|
||||||
position: relative;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sticky Footer
|
|
||||||
.outer-wrapper {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-wrapper {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
z-index: $main-z-index;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin-top: $content-spacing;
|
|
||||||
margin-bottom: $content-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert.hide {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ds-header-navbar-wrapper {
|
|
||||||
z-index: $nav-z-index;
|
|
||||||
}
|
|
||||||
|
|
||||||
ds-admin-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
z-index: $sidebar-z-index;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ds-full-screen-loader {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
@@ -32,6 +32,9 @@ import { storeModuleConfig } from './app.reducer';
|
|||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
import { authReducer } from './core/auth/auth.reducer';
|
import { authReducer } from './core/auth/auth.reducer';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
||||||
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -48,9 +51,7 @@ describe('App component', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForAsync beforeEach
|
const defaultTestBedConf = {
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
return TestBed.configureTestingModule({
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||||
@@ -74,12 +75,17 @@ describe('App component', () => {
|
|||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
{ provide: LocaleService, useValue: getMockLocaleService() },
|
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
AppComponent,
|
AppComponent,
|
||||||
RouteService
|
RouteService
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// waitForAsync beforeEach
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
return TestBed.configureTestingModule(defaultTestBedConf);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// synchronous beforeEach
|
// synchronous beforeEach
|
||||||
@@ -113,4 +119,59 @@ describe('App component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when GoogleAnalyticsService is provided', () => {
|
||||||
|
let googleAnalyticsSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule(defaultTestBedConf);
|
||||||
|
googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [
|
||||||
|
'addTrackingIdToPage',
|
||||||
|
]);
|
||||||
|
TestBed.overrideProvider(GoogleAnalyticsService, {useValue: googleAnalyticsSpy});
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(comp).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the constructor', () => {
|
||||||
|
it('should call googleAnalyticsService.addTrackingIdToPage()', () => {
|
||||||
|
expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when ThemeService returns a custom theme', () => {
|
||||||
|
let document;
|
||||||
|
let headSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule(defaultTestBedConf);
|
||||||
|
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
||||||
|
document = TestBed.inject(DOCUMENT);
|
||||||
|
headSpy = jasmine.createSpyObj('head', ['appendChild']);
|
||||||
|
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
|
||||||
|
fixture = TestBed.createComponent(AppComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append a link element with the correct attributes to the head element', () => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.setAttribute('rel', 'stylesheet');
|
||||||
|
link.setAttribute('type', 'text/css');
|
||||||
|
link.setAttribute('class', 'theme-css');
|
||||||
|
link.setAttribute('href', '/custom-theme.css');
|
||||||
|
|
||||||
|
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,12 +5,12 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
HostListener,
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
OnInit, Optional,
|
OnInit,
|
||||||
ViewEncapsulation
|
Optional,
|
||||||
} from '@angular/core';
|
} 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 { BehaviorSubject, 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 { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||||
@@ -23,24 +23,25 @@ import { isAuthenticationBlocking } from './core/auth/selectors';
|
|||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
import { MenuID } from './shared/menu/initial-menus-state';
|
|
||||||
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 { ThemeConfig } from '../config/theme.model';
|
||||||
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';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
import { hasValue } from './shared/empty.util';
|
import { hasValue, isNotEmpty } from './shared/empty.util';
|
||||||
import { KlaroService } from './shared/cookies/klaro.service';
|
import { KlaroService } from './shared/cookies/klaro.service';
|
||||||
|
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
|
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
||||||
|
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
encapsulation: ViewEncapsulation.None,
|
|
||||||
animations: [slideSidebarPadding]
|
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, AfterViewInit {
|
export class AppComponent implements OnInit, AfterViewInit {
|
||||||
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
@@ -48,7 +49,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
slideSidebarOver: Observable<boolean>;
|
slideSidebarOver: Observable<boolean>;
|
||||||
collapsedSidebarWidth: Observable<string>;
|
collapsedSidebarWidth: Observable<string>;
|
||||||
totalSidebarWidth: Observable<string>;
|
totalSidebarWidth: Observable<string>;
|
||||||
theme: Observable<Theme> = of({} as any);
|
theme: Observable<ThemeConfig> = of({} as any);
|
||||||
notificationOptions = environment.notifications;
|
notificationOptions = environment.notifications;
|
||||||
models;
|
models;
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
|
@Inject(DOCUMENT) private document: any,
|
||||||
|
private themeService: ThemeService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private store: Store<HostWindowState>,
|
private store: Store<HostWindowState>,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
@@ -70,11 +73,23 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService,
|
private localeService: LocaleService,
|
||||||
@Optional() private cookiesService: KlaroService
|
@Optional() private cookiesService: KlaroService,
|
||||||
|
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/* Use models object so all decorators are actually called */
|
/* Use models object so all decorators are actually called */
|
||||||
this.models = models;
|
this.models = models;
|
||||||
|
|
||||||
|
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
||||||
|
if (hasValue(themeName)) {
|
||||||
|
this.setThemeCss(themeName);
|
||||||
|
} else if (hasValue(DEFAULT_THEME_CONFIG)) {
|
||||||
|
this.setThemeCss(DEFAULT_THEME_CONFIG.name);
|
||||||
|
} else {
|
||||||
|
this.setThemeCss(BASE_THEME_NAME);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Load all the languages that are defined as active from the config file
|
// Load all the languages that are defined as active from the config file
|
||||||
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
|
|
||||||
@@ -84,7 +99,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
// set the current language code
|
// set the current language code
|
||||||
this.localeService.setCurrentLanguageCode();
|
this.localeService.setCurrentLanguageCode();
|
||||||
|
|
||||||
angulartics2GoogleAnalytics.startTracking();
|
// analytics
|
||||||
|
if (hasValue(googleAnalyticsService)) {
|
||||||
|
googleAnalyticsService.addTrackingIdToPage();
|
||||||
|
}
|
||||||
angulartics2DSpace.startTracking();
|
angulartics2DSpace.startTracking();
|
||||||
|
|
||||||
metadata.listenForRouteChange();
|
metadata.listenForRouteChange();
|
||||||
@@ -111,17 +129,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
const color: string = environment.production ? 'red' : 'green';
|
const color: string = environment.production ? 'red' : 'green';
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||||
|
|
||||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
|
||||||
|
|
||||||
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
|
||||||
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
|
||||||
|
|
||||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
|
||||||
this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm())
|
|
||||||
.pipe(
|
|
||||||
map(([collapsed, mobile]) => collapsed || mobile)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeCSSVariables() {
|
private storeCSSVariables() {
|
||||||
@@ -172,4 +179,34 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.cookiesService.initialize();
|
this.cookiesService.initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the theme css file in <head>
|
||||||
|
*
|
||||||
|
* @param themeName The name of the new theme
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setThemeCss(themeName: string): void {
|
||||||
|
const head = this.document.getElementsByTagName('head')[0];
|
||||||
|
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
|
||||||
|
// automatically updated if we add nodes later
|
||||||
|
const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css'));
|
||||||
|
const link = this.document.createElement('link');
|
||||||
|
link.setAttribute('rel', 'stylesheet');
|
||||||
|
link.setAttribute('type', 'text/css');
|
||||||
|
link.setAttribute('class', 'theme-css');
|
||||||
|
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`);
|
||||||
|
// wait for the new css to download before removing the old one to prevent a
|
||||||
|
// flash of unstyled content
|
||||||
|
link.onload = () => {
|
||||||
|
if (isNotEmpty(currentThemeLinks)) {
|
||||||
|
currentThemeLinks.forEach((currentThemeLink: any) => {
|
||||||
|
if (hasValue(currentThemeLink)) {
|
||||||
|
currentThemeLink.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
head.appendChild(link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,13 @@ import { NotificationsEffects } from './shared/notifications/notifications.effec
|
|||||||
import { NavbarEffects } from './navbar/navbar.effects';
|
import { NavbarEffects } from './navbar/navbar.effects';
|
||||||
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
|
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
|
||||||
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
|
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
|
||||||
|
import { ThemeEffects } from './shared/theme-support/theme.effects';
|
||||||
|
|
||||||
export const appEffects = [
|
export const appEffects = [
|
||||||
StoreEffects,
|
StoreEffects,
|
||||||
NavbarEffects,
|
NavbarEffects,
|
||||||
NotificationsEffects,
|
NotificationsEffects,
|
||||||
SidebarEffects,
|
SidebarEffects,
|
||||||
|
ThemeEffects,
|
||||||
RelationshipEffects
|
RelationshipEffects
|
||||||
];
|
];
|
||||||
|
@@ -43,6 +43,9 @@ import { ForbiddenComponent } from './forbidden/forbidden.component';
|
|||||||
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
import { AuthInterceptor } from './core/auth/auth.interceptor';
|
||||||
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
||||||
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||||
|
import { RootComponent } from './root/root.component';
|
||||||
|
import { ThemedRootComponent } from './root/themed-root.component';
|
||||||
|
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -65,6 +68,7 @@ const IMPORTS = [
|
|||||||
EffectsModule.forRoot(appEffects),
|
EffectsModule.forRoot(appEffects),
|
||||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||||
StoreRouterConnectingModule.forRoot(),
|
StoreRouterConnectingModule.forRoot(),
|
||||||
|
ThemedEntryComponentModule.withEntryComponents(),
|
||||||
];
|
];
|
||||||
|
|
||||||
IMPORTS.push(
|
IMPORTS.push(
|
||||||
@@ -120,6 +124,8 @@ const PROVIDERS = [
|
|||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
RootComponent,
|
||||||
|
ThemedRootComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
HeaderNavbarWrapperComponent,
|
HeaderNavbarWrapperComponent,
|
||||||
AdminSidebarComponent,
|
AdminSidebarComponent,
|
||||||
@@ -135,7 +141,6 @@ const DECLARATIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
AppComponent
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -150,7 +155,8 @@ const EXPORTS = [
|
|||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS,
|
||||||
|
...DECLARATIONS,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
@@ -12,7 +12,10 @@ import {
|
|||||||
metadataRegistryReducer,
|
metadataRegistryReducer,
|
||||||
MetadataRegistryState
|
MetadataRegistryState
|
||||||
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||||
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
import {
|
||||||
|
CommunityListReducer,
|
||||||
|
CommunityListState
|
||||||
|
} from './community-list-page/community-list.reducer';
|
||||||
import { hasValue } from './shared/empty.util';
|
import { hasValue } from './shared/empty.util';
|
||||||
import {
|
import {
|
||||||
NameVariantListsState,
|
NameVariantListsState,
|
||||||
@@ -20,19 +23,32 @@ import {
|
|||||||
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||||
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
import {
|
||||||
|
notificationsReducer,
|
||||||
|
NotificationsState
|
||||||
|
} from './shared/notifications/notifications.reducers';
|
||||||
import {
|
import {
|
||||||
selectableListReducer,
|
selectableListReducer,
|
||||||
SelectableListsState
|
SelectableListsState
|
||||||
} from './shared/object-list/selectable-list/selectable-list.reducer';
|
} from './shared/object-list/selectable-list/selectable-list.reducer';
|
||||||
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
import {
|
||||||
|
ObjectSelectionListState,
|
||||||
|
objectSelectionReducer
|
||||||
|
} from './shared/object-select/object-select.reducer';
|
||||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||||
|
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
|
import {
|
||||||
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
|
filterReducer,
|
||||||
|
SearchFiltersState
|
||||||
|
} from './shared/search/search-filters/search-filter/search-filter.reducer';
|
||||||
|
import {
|
||||||
|
sidebarFilterReducer,
|
||||||
|
SidebarFiltersState
|
||||||
|
} from './shared/sidebar/filter/sidebar-filter.reducer';
|
||||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
|
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
@@ -45,6 +61,7 @@ export interface AppState {
|
|||||||
searchFilter: SearchFiltersState;
|
searchFilter: SearchFiltersState;
|
||||||
truncatable: TruncatablesState;
|
truncatable: TruncatablesState;
|
||||||
cssVariables: CSSVariablesState;
|
cssVariables: CSSVariablesState;
|
||||||
|
theme: ThemeState;
|
||||||
menus: MenusState;
|
menus: MenusState;
|
||||||
objectSelection: ObjectSelectionListState;
|
objectSelection: ObjectSelectionListState;
|
||||||
selectableLists: SelectableListsState;
|
selectableLists: SelectableListsState;
|
||||||
@@ -65,6 +82,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
truncatable: truncatableReducer,
|
truncatable: truncatableReducer,
|
||||||
cssVariables: cssVariablesReducer,
|
cssVariables: cssVariablesReducer,
|
||||||
|
theme: themeReducer,
|
||||||
menus: menusReducer,
|
menus: menusReducer,
|
||||||
objectSelection: objectSelectionReducer,
|
objectSelection: objectSelectionReducer,
|
||||||
selectableLists: selectableListReducer,
|
selectableLists: selectableListReducer,
|
||||||
|
@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
|||||||
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
import { CollectionDataService } from '../data/collection-data.service';
|
import { CollectionDataService } from '../data/collection-data.service';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../+collection-page/collection-page.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a Collection
|
* The class that resolves the BreadcrumbConfig object for a Collection
|
||||||
@@ -22,10 +23,6 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collecti
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
get followLinks(): FollowLinkConfig<Collection>[] {
|
get followLinks(): FollowLinkConfig<Collection>[] {
|
||||||
return [
|
return COLLECTION_PAGE_LINKS_TO_FOLLOW;
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
|
||||||
followLink('parentCommunity')
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
|||||||
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
import { CommunityDataService } from '../data/community-data.service';
|
import { CommunityDataService } from '../data/community-data.service';
|
||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../+community-page/community-page.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for a Community
|
* The class that resolves the BreadcrumbConfig object for a Community
|
||||||
@@ -22,8 +23,6 @@ export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
get followLinks(): FollowLinkConfig<Community>[] {
|
get followLinks(): FollowLinkConfig<Community>[] {
|
||||||
return [
|
return COMMUNITY_PAGE_LINKS_TO_FOLLOW;
|
||||||
followLink('parentCommunity')
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResourc
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to recursively calculate the breadcrumbs
|
* Method to recursively calculate the breadcrumbs
|
||||||
* This method returns the name and url of the key and all its parent DSO's recursively, top down
|
* This method returns the name and url of the key and all its parent DSOs recursively, top down
|
||||||
* @param key The key (a DSpaceObject) used to resolve the breadcrumb
|
* @param key The key (a DSpaceObject) used to resolve the breadcrumb
|
||||||
* @param url The url to use as a link for this breadcrumb
|
* @param url The url to use as a link for this breadcrumb
|
||||||
*/
|
*/
|
||||||
|
@@ -20,7 +20,7 @@ export class DSONameService {
|
|||||||
*
|
*
|
||||||
* With only two exceptions those solutions seem overkill for now.
|
* With only two exceptions those solutions seem overkill for now.
|
||||||
*/
|
*/
|
||||||
private factories = {
|
private readonly factories = {
|
||||||
Person: (dso: DSpaceObject): string => {
|
Person: (dso: DSpaceObject): string => {
|
||||||
return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`;
|
return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`;
|
||||||
},
|
},
|
||||||
|
@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
|||||||
import { ItemDataService } from '../data/item-data.service';
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
||||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves the BreadcrumbConfig object for an Item
|
* The class that resolves the BreadcrumbConfig object for an Item
|
||||||
@@ -22,13 +23,6 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
|
|||||||
* Requesting them as embeds will limit the number of requests
|
* Requesting them as embeds will limit the number of requests
|
||||||
*/
|
*/
|
||||||
get followLinks(): FollowLinkConfig<Item>[] {
|
get followLinks(): FollowLinkConfig<Item>[] {
|
||||||
return [
|
return ITEM_PAGE_LINKS_TO_FOLLOW;
|
||||||
followLink('owningCollection', undefined, true, true, true,
|
|
||||||
followLink('parentCommunity', undefined, true, true, true,
|
|
||||||
followLink('parentCommunity'))
|
|
||||||
),
|
|
||||||
followLink('bundles'),
|
|
||||||
followLink('relationships')
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/app/core/cache/builders/link.service.ts
vendored
32
src/app/core/cache/builders/link.service.ts
vendored
@@ -3,7 +3,15 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
|||||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { GenericConstructor } from '../../shared/generic-constructor';
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators';
|
import {
|
||||||
|
getDataServiceFor,
|
||||||
|
getLinkDefinition,
|
||||||
|
getLinkDefinitions,
|
||||||
|
LinkDefinition
|
||||||
|
} from './build-decorators';
|
||||||
|
import { RemoteData } from '../../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { EMPTY } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Service to handle the resolving and removing
|
* A Service to handle the resolving and removing
|
||||||
@@ -33,12 +41,14 @@ export class LinkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the given {@link FollowLinkConfig} for the given model
|
* Resolve the given {@link FollowLinkConfig} for the given model and return the result. This does
|
||||||
|
* not attach the link result to the property on the model. Useful when you're working with a
|
||||||
|
* readonly object
|
||||||
*
|
*
|
||||||
* @param model the {@link HALResource} to resolve the link for
|
* @param model the {@link HALResource} to resolve the link for
|
||||||
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||||
*/
|
*/
|
||||||
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
|
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
|
||||||
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
|
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
|
||||||
|
|
||||||
if (hasNoValue(matchingLinkDef)) {
|
if (hasNoValue(matchingLinkDef)) {
|
||||||
@@ -61,9 +71,9 @@ export class LinkService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (matchingLinkDef.isList) {
|
if (matchingLinkDef.isList) {
|
||||||
model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
return service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
||||||
} else {
|
} else {
|
||||||
model[linkToFollow.name] = service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`);
|
console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`);
|
||||||
@@ -71,6 +81,18 @@ export class LinkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the given {@link FollowLinkConfig} for the given model and return the model with the
|
||||||
|
* link property attached.
|
||||||
|
*
|
||||||
|
* @param model the {@link HALResource} to resolve the link for
|
||||||
|
* @param linkToFollow the {@link FollowLinkConfig} to resolve
|
||||||
|
*/
|
||||||
|
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
|
||||||
|
model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,9 +13,14 @@ import { RestRequestMethod } from '../data/rest-request-method';
|
|||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions';
|
import {
|
||||||
|
CommitSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
||||||
import { storeModuleConfig } from '../../app.reducer';
|
import { storeModuleConfig } from '../../app.reducer';
|
||||||
|
import { NoOpAction } from '../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
describe('ServerSyncBufferEffects', () => {
|
describe('ServerSyncBufferEffects', () => {
|
||||||
let ssbEffects: ServerSyncBufferEffects;
|
let ssbEffects: ServerSyncBufferEffects;
|
||||||
@@ -143,7 +148,7 @@ describe('ServerSyncBufferEffects', () => {
|
|||||||
payload: { method: RestRequestMethod.PATCH }
|
payload: { method: RestRequestMethod.PATCH }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const expected = cold('b', { b: { type: 'NO_ACTION' } });
|
const expected = cold('b', { b: new NoOpAction() });
|
||||||
|
|
||||||
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
@@ -21,6 +21,7 @@ import { RestRequestMethod } from '../data/rest-request-method';
|
|||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { ObjectCacheEntry } from './object-cache.reducer';
|
import { ObjectCacheEntry } from './object-cache.reducer';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { NoOpAction } from '../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerSyncBufferEffects {
|
export class ServerSyncBufferEffects {
|
||||||
@@ -80,7 +81,7 @@ export class ServerSyncBufferEffects {
|
|||||||
switchMap((array) => [...array, new EmptySSBAction(action.payload)])
|
switchMap((array) => [...array, new EmptySSBAction(action.payload)])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return observableOf({ type: 'NO_ACTION' });
|
return observableOf(new NoOpAction());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -8,16 +8,6 @@ export class AccessConditionOption {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The uuid of the Group this Access Condition applies to
|
|
||||||
*/
|
|
||||||
groupUUID: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The uuid of the Group that contains set of groups this Resource Policy applies to
|
|
||||||
*/
|
|
||||||
selectGroupUUID: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if this Access Condition has a start date
|
* A boolean representing if this Access Condition has a start date
|
||||||
*/
|
*/
|
||||||
|
@@ -11,10 +11,14 @@ import {
|
|||||||
RemoveFieldUpdateAction,
|
RemoveFieldUpdateAction,
|
||||||
RemoveObjectUpdatesAction
|
RemoveObjectUpdatesAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
|
import {
|
||||||
|
INotification,
|
||||||
|
Notification
|
||||||
|
} from '../../../shared/notifications/models/notification.model';
|
||||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NoOpAction } from '../../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
describe('ObjectUpdatesEffects', () => {
|
describe('ObjectUpdatesEffects', () => {
|
||||||
let updatesEffects: ObjectUpdatesEffects;
|
let updatesEffects: ObjectUpdatesEffects;
|
||||||
@@ -97,7 +101,7 @@ describe('ObjectUpdatesEffects', () => {
|
|||||||
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||||
actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) });
|
actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) });
|
||||||
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => {
|
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => {
|
||||||
expect(t).toEqual({ type: 'NO_ACTION' });
|
expect(t).toEqual(new NoOpAction());
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -3,7 +3,8 @@ import { Actions, Effect, ofType } from '@ngrx/effects';
|
|||||||
import {
|
import {
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
ObjectUpdatesAction,
|
ObjectUpdatesAction,
|
||||||
ObjectUpdatesActionTypes, RemoveAllObjectUpdatesAction,
|
ObjectUpdatesActionTypes,
|
||||||
|
RemoveAllObjectUpdatesAction,
|
||||||
RemoveObjectUpdatesAction
|
RemoveObjectUpdatesAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
RemoveNotificationAction
|
RemoveNotificationAction
|
||||||
} from '../../../shared/notifications/notifications.actions';
|
} from '../../../shared/notifications/notifications.actions';
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
|
import { NoOpAction } from '../../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NGRX effects for ObjectUpdatesActions
|
* NGRX effects for ObjectUpdatesActions
|
||||||
@@ -111,7 +113,7 @@ export class ObjectUpdatesEffects {
|
|||||||
map((updateAction: ObjectUpdatesAction) => {
|
map((updateAction: ObjectUpdatesAction) => {
|
||||||
if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) {
|
if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) {
|
||||||
// If someone reinstated, do nothing, just let the reinstating happen
|
// If someone reinstated, do nothing, just let the reinstating happen
|
||||||
return { type: 'NO_ACTION' };
|
return new NoOpAction();
|
||||||
}
|
}
|
||||||
// If someone performed another action, assume the user does not want to reinstate and remove all changes
|
// If someone performed another action, assume the user does not want to reinstate and remove all changes
|
||||||
return removeAction;
|
return removeAction;
|
||||||
|
@@ -9,10 +9,10 @@ import {
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { ItemType } from '../shared/item-relationships/item-type.model';
|
import { ItemType } from '../shared/item-relationships/item-type.model';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { buildPaginatedList } from './paginated-list.model';
|
|
||||||
import { RelationshipTypeService } from './relationship-type.service';
|
import { RelationshipTypeService } from './relationship-type.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { hasValueOperator } from '../../shared/empty.util';
|
||||||
|
|
||||||
describe('RelationshipTypeService', () => {
|
describe('RelationshipTypeService', () => {
|
||||||
let service: RelationshipTypeService;
|
let service: RelationshipTypeService;
|
||||||
@@ -62,7 +62,7 @@ describe('RelationshipTypeService', () => {
|
|||||||
rightType: createSuccessfulRemoteDataObject$(orgUnitType)
|
rightType: createSuccessfulRemoteDataObject$(orgUnitType)
|
||||||
});
|
});
|
||||||
|
|
||||||
buildList = createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [relationshipType1, relationshipType2]));
|
buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2]));
|
||||||
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
|
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
|
||||||
objectCache = Object.assign({
|
objectCache = Object.assign({
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
@@ -100,9 +100,10 @@ describe('RelationshipTypeService', () => {
|
|||||||
describe('getRelationshipTypeByLabelAndTypes', () => {
|
describe('getRelationshipTypeByLabelAndTypes', () => {
|
||||||
|
|
||||||
it('should return the type filtered by label and type strings', (done) => {
|
it('should return the type filtered by label and type strings', (done) => {
|
||||||
const expected = service.getRelationshipTypeByLabelAndTypes(relationshipType1.leftwardType, publicationTypeString, personTypeString);
|
service.getRelationshipTypeByLabelAndTypes(relationshipType1.leftwardType, publicationTypeString, personTypeString).pipe(
|
||||||
expected.subscribe((e) => {
|
hasValueOperator()
|
||||||
expect(e).toBe(relationshipType1);
|
).subscribe((e) => {
|
||||||
|
expect(e.id).toEqual(relationshipType1.id);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,9 +2,9 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { filter, find, map, mergeMap, switchMap } from 'rxjs/operators';
|
import { map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { isNotUndefined } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { dataService } from '../cache/builders/build-decorators';
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
@@ -15,7 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { ItemType } from '../shared/item-relationships/item-type.model';
|
import { ItemType } from '../shared/item-relationships/item-type.model';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type';
|
import { RELATIONSHIP_TYPE } from '../shared/item-relationships/relationship-type.resource-type';
|
||||||
import { getFirstSucceededRemoteData } from '../shared/operators';
|
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } 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';
|
||||||
@@ -23,6 +23,15 @@ import { PaginatedList } from './paginated-list.model';
|
|||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if one side of a RelationshipType is the ItemType with the given label
|
||||||
|
*
|
||||||
|
* @param typeRd the RemoteData for an ItemType
|
||||||
|
* @param label the label to check. e.g. Author
|
||||||
|
*/
|
||||||
|
const checkSide = (typeRd: RemoteData<ItemType>, label: string): boolean =>
|
||||||
|
typeRd.hasSucceeded && typeRd.payload.label === label;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all relationship type requests
|
* The service handling all relationship type requests
|
||||||
*/
|
*/
|
||||||
@@ -45,36 +54,70 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the RelationshipType for a relationship type by label
|
* Find a RelationshipType object by its label, and the types of the items on either side.
|
||||||
* @param label
|
*
|
||||||
|
* TODO this should be implemented as a rest endpoint, we shouldn't have logic on the client that
|
||||||
|
* requires using a huge page-size in order to process "everything".
|
||||||
|
*
|
||||||
|
* @param relationshipTypeLabel The name of the relationType we're looking for
|
||||||
|
* e.g. isAuthorOfPublication
|
||||||
|
* @param firstItemType The type of one of the sides of the relationship e.g. Publication
|
||||||
|
* @param secondItemType The type of the other side of the relationship e.g. Author
|
||||||
*/
|
*/
|
||||||
getRelationshipTypeByLabelAndTypes(label: string, firstType: string, secondType: string): Observable<RelationshipType> {
|
getRelationshipTypeByLabelAndTypes(relationshipTypeLabel: string, firstItemType: string, secondItemType: string): Observable<RelationshipType> {
|
||||||
|
// Retrieve all relationship types from the server in a single page
|
||||||
return this.findAll({ currentPage: 1, elementsPerPage: 9999 }, true, true, followLink('leftType'), followLink('rightType'))
|
return this.findAll({ currentPage: 1, elementsPerPage: 9999 }, true, true, followLink('leftType'), followLink('rightType'))
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
/* Flatten the page so we can treat it like an observable */
|
// Emit each type in the page array separately
|
||||||
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
||||||
mergeMap((type: RelationshipType) => {
|
// Check each type individually, to see if it matches the provided types
|
||||||
if (type.leftwardType === label) {
|
mergeMap((relationshipType: RelationshipType) => {
|
||||||
return this.checkType(type, firstType, secondType);
|
if (relationshipType.leftwardType === relationshipTypeLabel) {
|
||||||
} else if (type.rightwardType === label) {
|
return this.checkType(relationshipType, firstItemType, secondItemType);
|
||||||
return this.checkType(type, secondType, firstType);
|
} else if (relationshipType.rightwardType === relationshipTypeLabel) {
|
||||||
|
return this.checkType(relationshipType, secondItemType, firstItemType);
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [null];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// Wait for all types to be checked and emit once, with the results combined back into an
|
||||||
|
// array
|
||||||
|
toArray(),
|
||||||
|
// Look for a match in the array and emit it if found, or null if one isn't found
|
||||||
|
map((types: RelationshipType[]) => {
|
||||||
|
const match = types.find((type: RelationshipType) => hasValue(type));
|
||||||
|
if (hasValue(match)) {
|
||||||
|
return match;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if relationship type matches the given types
|
/**
|
||||||
// returns a void observable if there's not match
|
* Check if the given RelationshipType has the given itemTypes on its left and right sides.
|
||||||
// returns an observable that emits the relationship type when there is a match
|
* Returns an observable of the given RelationshipType if it matches, null if it doesn't
|
||||||
private checkType(type: RelationshipType, firstType: string, secondType: string): Observable<RelationshipType> {
|
*
|
||||||
const entityTypes = observableCombineLatest([type.leftType.pipe(getFirstSucceededRemoteData()), type.rightType.pipe(getFirstSucceededRemoteData())]);
|
* @param type The RelationshipType to check
|
||||||
return entityTypes.pipe(
|
* @param leftItemType The item type that should be on the left side
|
||||||
find(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType),
|
* @param rightItemType The item type that should be on the right side
|
||||||
filter((types) => isNotUndefined(types)),
|
* @private
|
||||||
map(() => type)
|
*/
|
||||||
|
private checkType(type: RelationshipType, leftItemType: string, rightItemType: string): Observable<RelationshipType> {
|
||||||
|
return observableCombineLatest([
|
||||||
|
type.leftType.pipe(getFirstCompletedRemoteData()),
|
||||||
|
type.rightType.pipe(getFirstCompletedRemoteData())
|
||||||
|
]).pipe(
|
||||||
|
map(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => {
|
||||||
|
if (checkSide(leftTypeRD, leftItemType) && checkSide(rightTypeRD, rightItemType)
|
||||||
|
) {
|
||||||
|
return type;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -63,10 +63,10 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
* @param query Query of search
|
* @param query Query of search
|
||||||
* @param options Options of search request
|
* @param options Options of search request
|
||||||
*/
|
*/
|
||||||
public searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case 'metadata':
|
case 'metadata':
|
||||||
return this.getEpeopleByMetadata(query.trim(), options);
|
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
|
||||||
case 'email':
|
case 'email':
|
||||||
return this.getEPersonByEmail(query.trim()).pipe(
|
return this.getEPersonByEmail(query.trim()).pipe(
|
||||||
map((rd: RemoteData<EPerson | NoContent>) => {
|
map((rd: RemoteData<EPerson | NoContent>) => {
|
||||||
@@ -100,7 +100,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return this.getEpeopleByMetadata(query.trim(), options);
|
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,5 +13,9 @@ export class EpersonDtoModel {
|
|||||||
* Whether or not the linked EPerson is able to be deleted
|
* Whether or not the linked EPerson is able to be deleted
|
||||||
*/
|
*/
|
||||||
public ableToDelete: boolean;
|
public ableToDelete: boolean;
|
||||||
|
/**
|
||||||
|
* Whether or not this EPerson is member of group on page it is being used on
|
||||||
|
*/
|
||||||
|
public memberOfGroup: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
|
import { PaginatedList } from '../../data/paginated-list.model';
|
||||||
|
import { EPerson } from './eperson.model';
|
||||||
import { Group } from './group.model';
|
import { Group } from './group.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class serves as a Data Transfer Model that contains the Group and whether or not it's able to be deleted
|
* This class serves as a Data Transfer Model that contains the Group, whether or not it's able to be deleted and its members
|
||||||
*/
|
*/
|
||||||
export class GroupDtoModel {
|
export class GroupDtoModel {
|
||||||
|
|
||||||
@@ -9,9 +11,20 @@ export class GroupDtoModel {
|
|||||||
* The Group linked to this object
|
* The Group linked to this object
|
||||||
*/
|
*/
|
||||||
public group: Group;
|
public group: Group;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the linked Group is able to be deleted
|
* Whether or not the linked Group is able to be deleted
|
||||||
*/
|
*/
|
||||||
public ableToDelete: boolean;
|
public ableToDelete: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subgroups of this group
|
||||||
|
*/
|
||||||
|
public subgroups: PaginatedList<Group>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of members of this group
|
||||||
|
*/
|
||||||
|
public epersons: PaginatedList<EPerson>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
import { autoserialize, autoserializeAs, deserialize, inheritSerialization } from 'cerialize';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { link, typedObject } from '../../cache/builders/build-decorators';
|
import { link, typedObject } from '../../cache/builders/build-decorators';
|
||||||
import { PaginatedList } from '../../data/paginated-list.model';
|
import { PaginatedList } from '../../data/paginated-list.model';
|
||||||
@@ -10,12 +10,20 @@ import { HALLink } from '../../shared/hal-link.model';
|
|||||||
import { EPerson } from './eperson.model';
|
import { EPerson } from './eperson.model';
|
||||||
import { EPERSON } from './eperson.resource-type';
|
import { EPERSON } from './eperson.resource-type';
|
||||||
import { GROUP } from './group.resource-type';
|
import { GROUP } from './group.resource-type';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
|
||||||
@typedObject
|
@typedObject
|
||||||
@inheritSerialization(DSpaceObject)
|
@inheritSerialization(DSpaceObject)
|
||||||
export class Group extends DSpaceObject {
|
export class Group extends DSpaceObject {
|
||||||
static type = GROUP;
|
static type = GROUP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string representing the unique name of this Group
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserializeAs('name')
|
||||||
|
protected _name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string representing the unique handle of this Group
|
* A string representing the unique handle of this Group
|
||||||
*/
|
*/
|
||||||
|
@@ -8,6 +8,7 @@ import { Item } from '../shared/item.model';
|
|||||||
import { AddToIndexAction } from './index.actions';
|
import { AddToIndexAction } from './index.actions';
|
||||||
import { IndexName } from './index.reducer';
|
import { IndexName } from './index.reducer';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { NoOpAction } from '../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
describe('ObjectUpdatesEffects', () => {
|
describe('ObjectUpdatesEffects', () => {
|
||||||
let indexEffects: UUIDIndexEffects;
|
let indexEffects: UUIDIndexEffects;
|
||||||
@@ -79,14 +80,14 @@ describe('ObjectUpdatesEffects', () => {
|
|||||||
it('should emit NO_ACTION when a AddToObjectCacheAction without an alternativeLink is dispatched', () => {
|
it('should emit NO_ACTION when a AddToObjectCacheAction without an alternativeLink is dispatched', () => {
|
||||||
action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, undefined);
|
action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, undefined);
|
||||||
actions = hot('--a-', { a: action });
|
actions = hot('--a-', { a: action });
|
||||||
const expected = cold('--b-', { b: { type: 'NO_ACTION' } });
|
const expected = cold('--b-', { b: new NoOpAction() });
|
||||||
expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected);
|
expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit NO_ACTION when a AddToObjectCacheAction with an alternativeLink that\'s the same as the objectToCache\'s selfLink is dispatched', () => {
|
it('should emit NO_ACTION when a AddToObjectCacheAction with an alternativeLink that\'s the same as the objectToCache\'s selfLink is dispatched', () => {
|
||||||
action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, objectToCache._links.self.href);
|
action = new AddToObjectCacheAction(objectToCache, timeCompleted, msToLive, requestUUID, objectToCache._links.self.href);
|
||||||
actions = hot('--a-', { a: action });
|
actions = hot('--a-', { a: action });
|
||||||
const expected = cold('--b-', { b: { type: 'NO_ACTION' } });
|
const expected = cold('--b-', { b: new NoOpAction() });
|
||||||
expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected);
|
expect(indexEffects.addAlternativeObjectLink$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,6 +19,7 @@ import { RestRequestMethod } from '../data/rest-request-method';
|
|||||||
import { getUrlWithoutEmbedParams, uuidFromHrefSelector } from './index.selectors';
|
import { getUrlWithoutEmbedParams, uuidFromHrefSelector } from './index.selectors';
|
||||||
import { Store, select } from '@ngrx/store';
|
import { Store, select } from '@ngrx/store';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { NoOpAction } from '../../shared/ngrx/no-op.action';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UUIDIndexEffects {
|
export class UUIDIndexEffects {
|
||||||
@@ -53,7 +54,7 @@ export class UUIDIndexEffects {
|
|||||||
selfLink
|
selfLink
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return { type: 'NO_ACTION' };
|
return new NoOpAction();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
27
src/app/core/resolving/resolver.actions.ts
Normal file
27
src/app/core/resolving/resolver.actions.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
|
export const ResolverActionTypes = {
|
||||||
|
RESOLVED: type('dspace/resolver/RESOLVED')
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that indicates a route object has been resolved.
|
||||||
|
*
|
||||||
|
* It isn't used in a reducer for now. Its purpose is to be able to be notified that an object
|
||||||
|
* has been resolved in an effect.
|
||||||
|
*/
|
||||||
|
export class ResolvedAction implements Action {
|
||||||
|
type = ResolverActionTypes.RESOLVED;
|
||||||
|
payload: {
|
||||||
|
url: string,
|
||||||
|
dso: DSpaceObject
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(url: string, dso: DSpaceObject) {
|
||||||
|
this.payload = { url, dso };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResolverAction = ResolvedAction;
|
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export enum Context {
|
export enum Context {
|
||||||
Undefined = 'undefined',
|
Any = 'undefined',
|
||||||
ItemPage = 'itemPage',
|
ItemPage = 'itemPage',
|
||||||
Search = 'search',
|
Search = 'search',
|
||||||
Workflow = 'workflow',
|
Workflow = 'workflow',
|
||||||
|
@@ -29,7 +29,7 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
|
|||||||
|
|
||||||
@excludeFromEquals
|
@excludeFromEquals
|
||||||
@deserializeAs('name')
|
@deserializeAs('name')
|
||||||
private _name: string;
|
protected _name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The human-readable identifier of this DSpaceObject
|
* The human-readable identifier of this DSpaceObject
|
||||||
|
@@ -13,11 +13,6 @@ export class SubmissionUploadFileAccessConditionObject {
|
|||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The access group UUID defined in this access condition
|
|
||||||
*/
|
|
||||||
groupUUID: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible start date of the access condition
|
* Possible start date of the access condition
|
||||||
*/
|
*/
|
||||||
|
@@ -8,9 +8,16 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { ClaimedTaskDataService } from './claimed-task-data.service';
|
import { ClaimedTaskDataService } from './claimed-task-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
describe('ClaimedTaskDataService', () => {
|
describe('ClaimedTaskDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
let service: ClaimedTaskDataService;
|
let service: ClaimedTaskDataService;
|
||||||
let options: HttpOptions;
|
let options: HttpOptions;
|
||||||
const taskEndpoint = 'https://rest.api/task';
|
const taskEndpoint = 'https://rest.api/task';
|
||||||
@@ -45,6 +52,7 @@ describe('ClaimedTaskDataService', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
options = Object.create({});
|
options = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -68,6 +76,24 @@ describe('ClaimedTaskDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('claimTask', () => {
|
||||||
|
|
||||||
|
it('should call postToEndpoint method', () => {
|
||||||
|
|
||||||
|
spyOn(service, 'postToEndpoint').and.returnValue(observableOf(null));
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.claimTask('scopeId', 'poolTaskHref').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
const postToEndpointOptions: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
postToEndpointOptions.headers = headers;
|
||||||
|
|
||||||
|
expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, 'poolTaskHref', null, postToEndpointOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('returnToPoolTask', () => {
|
describe('returnToPoolTask', () => {
|
||||||
it('should call deleteById method', () => {
|
it('should call deleteById method', () => {
|
||||||
const scopeId = '1234';
|
const scopeId = '1234';
|
||||||
@@ -79,4 +105,21 @@ describe('ClaimedTaskDataService', () => {
|
|||||||
expect(service.deleteById).toHaveBeenCalledWith(linkPath, scopeId, options);
|
expect(service.deleteById).toHaveBeenCalledWith(linkPath, scopeId, options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findByItem', () => {
|
||||||
|
|
||||||
|
it('should call searchTask method', () => {
|
||||||
|
spyOn((service as any), 'searchTask').and.returnValue(observableOf(createSuccessfulRemoteDataObject$({})));
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.findByItem('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
const findListOptions = new FindListOptions();
|
||||||
|
findListOptions.searchParams = [
|
||||||
|
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -15,6 +15,11 @@ import { ClaimedTask } from './models/claimed-task-object.model';
|
|||||||
import { CLAIMED_TASK } from './models/claimed-task-object.resource-type';
|
import { CLAIMED_TASK } from './models/claimed-task-object.resource-type';
|
||||||
import { ProcessTaskResponse } from './models/process-task-response';
|
import { ProcessTaskResponse } from './models/process-task-response';
|
||||||
import { TasksService } from './tasks.service';
|
import { TasksService } from './tasks.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { getFirstSucceededRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all REST requests for ClaimedTask
|
* The service handling all REST requests for ClaimedTask
|
||||||
@@ -23,7 +28,7 @@ import { TasksService } from './tasks.service';
|
|||||||
@dataService(CLAIMED_TASK)
|
@dataService(CLAIMED_TASK)
|
||||||
export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
|
export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
|
||||||
|
|
||||||
protected responseMsToLive = 10 * 1000;
|
protected responseMsToLive = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The endpoint link name
|
* The endpoint link name
|
||||||
@@ -54,6 +59,24 @@ export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request to claim the given task
|
||||||
|
*
|
||||||
|
* @param scopeId
|
||||||
|
* The task id
|
||||||
|
* @param poolTaskHref
|
||||||
|
* The pool task Href
|
||||||
|
* @return {Observable<ProcessTaskResponse>}
|
||||||
|
* Emit the server response
|
||||||
|
*/
|
||||||
|
public claimTask(scopeId: string, poolTaskHref: string): Observable<ProcessTaskResponse> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
return this.postToEndpoint(this.linkPath, poolTaskHref, null, options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request for the given task
|
* Make a request for the given task
|
||||||
*
|
*
|
||||||
@@ -80,4 +103,19 @@ export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
|
|||||||
return this.deleteById(this.linkPath, scopeId, this.makeHttpOptions());
|
return this.deleteById(this.linkPath, scopeId, this.makeHttpOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a claimed task by item uuid.
|
||||||
|
* @param uuid
|
||||||
|
* The item uuid
|
||||||
|
* @return {Observable<RemoteData<ClaimedTask>>}
|
||||||
|
* The server response
|
||||||
|
*/
|
||||||
|
public findByItem(uuid: string): Observable<RemoteData<ClaimedTask>> {
|
||||||
|
const options = new FindListOptions();
|
||||||
|
options.searchParams = [
|
||||||
|
new RequestParam('uuid', uuid)
|
||||||
|
];
|
||||||
|
return this.searchTask('findByItem', options).pipe(getFirstSucceededRemoteData());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,9 +8,16 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { PoolTaskDataService } from './pool-task-data.service';
|
import { PoolTaskDataService } from './pool-task-data.service';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
describe('PoolTaskDataService', () => {
|
describe('PoolTaskDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
let service: PoolTaskDataService;
|
let service: PoolTaskDataService;
|
||||||
let options: HttpOptions;
|
let options: HttpOptions;
|
||||||
const taskEndpoint = 'https://rest.api/task';
|
const taskEndpoint = 'https://rest.api/task';
|
||||||
@@ -45,6 +52,7 @@ describe('PoolTaskDataService', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
options = Object.create({});
|
options = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
@@ -52,14 +60,33 @@ describe('PoolTaskDataService', () => {
|
|||||||
options.headers = headers;
|
options.headers = headers;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('claimTask', () => {
|
describe('findByItem', () => {
|
||||||
|
|
||||||
it('should call postToEndpoint method', () => {
|
it('should call searchTask method', () => {
|
||||||
spyOn(service, 'postToEndpoint');
|
spyOn((service as any), 'searchTask').and.returnValue(observableOf(createSuccessfulRemoteDataObject$({})));
|
||||||
const scopeId = '1234';
|
|
||||||
service.claimTask(scopeId);
|
scheduler.schedule(() => service.findByItem('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
const findListOptions = new FindListOptions();
|
||||||
|
findListOptions.searchParams = [
|
||||||
|
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPoolTaskEndpointById', () => {
|
||||||
|
|
||||||
|
it('should call getEndpointById method', () => {
|
||||||
|
spyOn(service, 'getEndpointById').and.returnValue(observableOf(null));
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getPoolTaskEndpointById('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getEndpointById).toHaveBeenCalledWith('a0db0fde-1d12-4d43-bd0d-0f43df8d823c');
|
||||||
|
|
||||||
expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, {}, scopeId, options);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -13,8 +13,11 @@ import { RequestService } from '../data/request.service';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { PoolTask } from './models/pool-task-object.model';
|
import { PoolTask } from './models/pool-task-object.model';
|
||||||
import { POOL_TASK } from './models/pool-task-object.resource-type';
|
import { POOL_TASK } from './models/pool-task-object.resource-type';
|
||||||
import { ProcessTaskResponse } from './models/process-task-response';
|
|
||||||
import { TasksService } from './tasks.service';
|
import { TasksService } from './tasks.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { FindListOptions } from '../data/request.models';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all REST requests for PoolTask
|
* The service handling all REST requests for PoolTask
|
||||||
@@ -28,7 +31,7 @@ export class PoolTaskDataService extends TasksService<PoolTask> {
|
|||||||
*/
|
*/
|
||||||
protected linkPath = 'pooltasks';
|
protected linkPath = 'pooltasks';
|
||||||
|
|
||||||
protected responseMsToLive = 10 * 1000;
|
protected responseMsToLive = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
@@ -56,14 +59,30 @@ export class PoolTaskDataService extends TasksService<PoolTask> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a request to claim the given task
|
* Search a pool task by item uuid.
|
||||||
*
|
* @param uuid
|
||||||
* @param scopeId
|
* The item uuid
|
||||||
* The task id
|
* @return {Observable<RemoteData<ClaimedTask>>}
|
||||||
* @return {Observable<ProcessTaskResponse>}
|
* The server response
|
||||||
* Emit the server response
|
|
||||||
*/
|
*/
|
||||||
public claimTask(scopeId: string): Observable<ProcessTaskResponse> {
|
public findByItem(uuid: string): Observable<RemoteData<PoolTask>> {
|
||||||
return this.postToEndpoint(this.linkPath, {}, scopeId, this.makeHttpOptions());
|
const options = new FindListOptions();
|
||||||
|
options.searchParams = [
|
||||||
|
new RequestParam('uuid', uuid)
|
||||||
|
];
|
||||||
|
return this.searchTask('findByItem', options).pipe(getFirstCompletedRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Href of the pool task
|
||||||
|
*
|
||||||
|
* @param poolTaskId
|
||||||
|
* the poolTask id
|
||||||
|
* @return {Observable<string>>}
|
||||||
|
* the Href
|
||||||
|
*/
|
||||||
|
public getPoolTaskEndpointById(poolTaskId): Observable<string> {
|
||||||
|
return this.getEndpointById(poolTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { TestScheduler } from 'rxjs/testing';
|
|||||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { TasksService } from './tasks.service';
|
import { TasksService } from './tasks.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
|
import { FindListOptions, TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { TaskObject } from './models/task-object.model';
|
import { TaskObject } from './models/task-object.model';
|
||||||
@@ -17,8 +17,11 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||||
import { ChangeAnalyzer } from '../data/change-analyzer';
|
import { ChangeAnalyzer } from '../data/change-analyzer';
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ describe('TasksService', () => {
|
|||||||
const requestService = getMockRequestService();
|
const requestService = getMockRequestService();
|
||||||
const halService: any = new HALEndpointServiceStub(taskEndpoint);
|
const halService: any = new HALEndpointServiceStub(taskEndpoint);
|
||||||
const rdbService = getMockRemoteDataBuildService();
|
const rdbService = getMockRemoteDataBuildService();
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = new NotificationsServiceStub() as any;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
const comparator = new DummyChangeAnalyzer() as any;
|
const comparator = new DummyChangeAnalyzer() as any;
|
||||||
const objectCache = {
|
const objectCache = {
|
||||||
@@ -118,4 +121,38 @@ describe('TasksService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('searchTask', () => {
|
||||||
|
|
||||||
|
it('should call findByHref with the href generated by getSearchByHref', () => {
|
||||||
|
|
||||||
|
spyOn(service, 'getSearchByHref').and.returnValue(observableOf('generatedHref'));
|
||||||
|
spyOn(service, 'findByHref').and.returnValue(of(null));
|
||||||
|
|
||||||
|
const followLinks = {};
|
||||||
|
const options = new FindListOptions();
|
||||||
|
options.searchParams = [];
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.searchTask('method', options, followLinks as any).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getSearchByHref).toHaveBeenCalledWith('method', options, followLinks as any);
|
||||||
|
expect(service.findByHref).toHaveBeenCalledWith('generatedHref', false, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEndpointById', () => {
|
||||||
|
|
||||||
|
it('should call halService.getEndpoint and then getEndpointByIDHref', () => {
|
||||||
|
|
||||||
|
spyOn(halService, 'getEndpoint').and.returnValue(observableOf('generatedHref'));
|
||||||
|
spyOn(service, 'getEndpointByIDHref').and.returnValue(null);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getEndpointById('scopeId').subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(halService.getEndpoint).toHaveBeenCalledWith(service.getLinkPath());
|
||||||
|
expect(service.getEndpointByIDHref).toHaveBeenCalledWith('generatedHref', 'scopeId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,43 +1,29 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataService } from '../data/data.service';
|
import { DataService } from '../data/data.service';
|
||||||
import { DeleteRequest, PostRequest, TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
|
import {
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
DeleteRequest,
|
||||||
|
FindListOptions,
|
||||||
|
PostRequest,
|
||||||
|
TaskDeleteRequest,
|
||||||
|
TaskPostRequest
|
||||||
|
} from '../data/request.models';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||||
import { ProcessTaskResponse } from './models/process-task-response';
|
import { ProcessTaskResponse } from './models/process-task-response';
|
||||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract class that provides methods to handle task requests.
|
* An abstract class that provides methods to handle task requests.
|
||||||
*/
|
*/
|
||||||
export abstract class TasksService<T extends CacheableObject> extends DataService<T> {
|
export abstract class TasksService<T extends CacheableObject> extends DataService<T> {
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a RestRequest
|
|
||||||
*
|
|
||||||
* @param requestId
|
|
||||||
* The base endpoint for the type of object
|
|
||||||
* @return Observable<ProcessTaskResponse>
|
|
||||||
* server response
|
|
||||||
*/
|
|
||||||
protected fetchRequest(requestId: string): Observable<ProcessTaskResponse> {
|
|
||||||
return this.rdbService.buildFromRequestUUID(requestId).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((response: RemoteData<any>) => {
|
|
||||||
if (response.hasFailed) {
|
|
||||||
return new ProcessTaskResponse(false, response.statusCode, response.errorMessage);
|
|
||||||
} else {
|
|
||||||
return new ProcessTaskResponse(true, response.statusCode);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the HREF for a specific submission object based on its identifier
|
* Create the HREF for a specific submission object based on its identifier
|
||||||
*
|
*
|
||||||
@@ -46,7 +32,7 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
|
|||||||
* @param resourceID
|
* @param resourceID
|
||||||
* The identifier for the object
|
* The identifier for the object
|
||||||
*/
|
*/
|
||||||
protected getEndpointByIDHref(endpoint, resourceID): string {
|
getEndpointByIDHref(endpoint, resourceID): string {
|
||||||
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
|
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +76,67 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
|
|||||||
*/
|
*/
|
||||||
public deleteById(linkPath: string, scopeId: string, options?: HttpOptions): Observable<ProcessTaskResponse> {
|
public deleteById(linkPath: string, scopeId: string, options?: HttpOptions): Observable<ProcessTaskResponse> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
return this.halService.getEndpoint(linkPath || this.linkPath).pipe(
|
return this.getEndpointById(scopeId, linkPath).pipe(
|
||||||
filter((href: string) => isNotEmpty(href)),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
|
|
||||||
map((endpointURL: string) => new TaskDeleteRequest(requestId, endpointURL, null, options)),
|
map((endpointURL: string) => new TaskDeleteRequest(requestId, endpointURL, null, options)),
|
||||||
tap((request: DeleteRequest) => this.requestService.send(request)),
|
tap((request: DeleteRequest) => this.requestService.send(request)),
|
||||||
mergeMap((request: DeleteRequest) => this.fetchRequest(requestId)),
|
mergeMap((request: DeleteRequest) => this.fetchRequest(requestId)),
|
||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint of a task by scopeId.
|
||||||
|
* @param linkPath
|
||||||
|
* @param scopeId
|
||||||
|
*/
|
||||||
|
public getEndpointById(scopeId: string, linkPath?: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(linkPath || this.linkPath).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a task.
|
||||||
|
* @param searchMethod
|
||||||
|
* the search method
|
||||||
|
* @param options
|
||||||
|
* the find list options
|
||||||
|
* @param linksToFollow
|
||||||
|
* links to follow
|
||||||
|
*/
|
||||||
|
public searchTask(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||||
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
return hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
mergeMap((href) => this.findByHref(href, false, true).pipe(
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
filter((rd: RemoteData<T>) => !rd.isSuccessStale),
|
||||||
|
tap(() => this.requestService.setStaleByHrefSubstring(href)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a RestRequest
|
||||||
|
*
|
||||||
|
* @param requestId
|
||||||
|
* The base endpoint for the type of object
|
||||||
|
* @return Observable<ProcessTaskResponse>
|
||||||
|
* server response
|
||||||
|
*/
|
||||||
|
protected fetchRequest(requestId: string): Observable<ProcessTaskResponse> {
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((response: RemoteData<any>) => {
|
||||||
|
if (response.hasFailed) {
|
||||||
|
return new ProcessTaskResponse(false, response.statusCode, response.errorMessage);
|
||||||
|
} else {
|
||||||
|
return new ProcessTaskResponse(true, response.statusCode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new HttpOptions
|
* Create a new HttpOptions
|
||||||
*/
|
*/
|
||||||
|
@@ -1,32 +1,20 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
import { HttpHeaders, HTTP_INTERCEPTORS, HttpResponse, HttpXsrfTokenExtractor, HttpErrorResponse } from '@angular/common/http';
|
import { HttpHeaders, HTTP_INTERCEPTORS, HttpXsrfTokenExtractor } from '@angular/common/http';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
|
||||||
import { XsrfInterceptor } from './xsrf.interceptor';
|
import { XsrfInterceptor } from './xsrf.interceptor';
|
||||||
|
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
|
||||||
/**
|
|
||||||
* A Mock TokenExtractor which just returns whatever token it is initialized with.
|
|
||||||
* This mock object is injected into our XsrfInterceptor, so that it always finds
|
|
||||||
* the same fake XSRF token.
|
|
||||||
*/
|
|
||||||
class MockTokenExtractor extends HttpXsrfTokenExtractor {
|
|
||||||
constructor(private token: string | null) { super(); }
|
|
||||||
|
|
||||||
getToken(): string | null { return this.token; }
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(`XsrfInterceptor`, () => {
|
describe(`XsrfInterceptor`, () => {
|
||||||
let service: DspaceRestService;
|
let service: DspaceRestService;
|
||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
let cookieService: CookieService;
|
let cookieService: CookieService;
|
||||||
|
|
||||||
// Create a MockTokenExtractor which always returns "test-token". This will
|
// mock XSRF token
|
||||||
// be used as the test HttpXsrfTokenExtractor, see below.
|
|
||||||
const testToken = 'test-token';
|
const testToken = 'test-token';
|
||||||
const mockTokenExtractor = new MockTokenExtractor(testToken);
|
|
||||||
|
|
||||||
// Mock payload/statuses are dummy content as we are not testing the results
|
// Mock payload/statuses are dummy content as we are not testing the results
|
||||||
// of any below requests. We are only testing for X-XSRF-TOKEN header.
|
// of any below requests. We are only testing for X-XSRF-TOKEN header.
|
||||||
@@ -46,7 +34,7 @@ describe(`XsrfInterceptor`, () => {
|
|||||||
useClass: XsrfInterceptor,
|
useClass: XsrfInterceptor,
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
{ provide: HttpXsrfTokenExtractor, useValue: mockTokenExtractor },
|
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock(testToken) },
|
||||||
{ provide: CookieService, useValue: new CookieServiceMock() }
|
{ provide: CookieService, useValue: new CookieServiceMock() }
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,26 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpXsrfTokenExtractor } from '@angular/common/http';
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
HttpEvent,
|
||||||
|
HttpHandler,
|
||||||
|
HttpInterceptor,
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
HttpXsrfTokenExtractor
|
||||||
|
} from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { tap, catchError } from 'rxjs/operators';
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||||
import { CookieService } from '../services/cookie.service';
|
import { CookieService } from '../services/cookie.service';
|
||||||
import { throwError } from 'rxjs';
|
import { throwError } from 'rxjs';
|
||||||
|
|
||||||
|
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
|
||||||
|
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
|
||||||
|
// Name of XSRF header we may receive in responses from backend
|
||||||
|
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
|
||||||
|
// Name of cookie where we store the XSRF token
|
||||||
|
export const XSRF_COOKIE = 'XSRF-TOKEN';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Http Interceptor intercepting Http Requests & Responses to
|
* Custom Http Interceptor intercepting Http Requests & Responses to
|
||||||
* exchange XSRF/CSRF tokens with the backend.
|
* exchange XSRF/CSRF tokens with the backend.
|
||||||
@@ -43,11 +58,6 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
|
|
||||||
const requestCsrfHeader = 'X-XSRF-TOKEN';
|
|
||||||
// Name of XSRF header we may receive in responses from backend
|
|
||||||
const responseCsrfHeader = 'DSPACE-XSRF-TOKEN';
|
|
||||||
|
|
||||||
// Ensure EVERY request from Angular includes "withCredentials: true".
|
// Ensure EVERY request from Angular includes "withCredentials: true".
|
||||||
// This allows Angular to receive & send cookies via a CORS request (to
|
// This allows Angular to receive & send cookies via a CORS request (to
|
||||||
// the backend). ONLY requests with credentials will:
|
// the backend). ONLY requests with credentials will:
|
||||||
@@ -71,8 +81,8 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
const token = this.tokenExtractor.getToken() as string;
|
const token = this.tokenExtractor.getToken() as string;
|
||||||
|
|
||||||
// send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
|
// send token in request's X-XSRF-TOKEN header (anti-CSRF security) to backend
|
||||||
if (token !== null && !req.headers.has(requestCsrfHeader)) {
|
if (token !== null && !req.headers.has(XSRF_REQUEST_HEADER)) {
|
||||||
req = req.clone({ headers: req.headers.set(requestCsrfHeader, token) });
|
req = req.clone({ headers: req.headers.set(XSRF_REQUEST_HEADER, token) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pass to next interceptor, but intercept EVERY response event as well
|
// Pass to next interceptor, but intercept EVERY response event as well
|
||||||
@@ -82,9 +92,9 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
if (response instanceof HttpResponse) {
|
if (response instanceof HttpResponse) {
|
||||||
// For every response that comes back, check for the custom
|
// For every response that comes back, check for the custom
|
||||||
// DSPACE-XSRF-TOKEN header sent from the backend.
|
// DSPACE-XSRF-TOKEN header sent from the backend.
|
||||||
if (response.headers.has(responseCsrfHeader)) {
|
if (response.headers.has(XSRF_RESPONSE_HEADER)) {
|
||||||
// value of header is a new XSRF token
|
// value of header is a new XSRF token
|
||||||
this.saveXsrfToken(response.headers.get(responseCsrfHeader));
|
this.saveXsrfToken(response.headers.get(XSRF_RESPONSE_HEADER));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -92,9 +102,9 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
if (error instanceof HttpErrorResponse) {
|
if (error instanceof HttpErrorResponse) {
|
||||||
// For every error that comes back, also check for the custom
|
// For every error that comes back, also check for the custom
|
||||||
// DSPACE-XSRF-TOKEN header sent from the backend.
|
// DSPACE-XSRF-TOKEN header sent from the backend.
|
||||||
if (error.headers.has(responseCsrfHeader)) {
|
if (error.headers.has(XSRF_RESPONSE_HEADER)) {
|
||||||
// value of header is a new XSRF token
|
// value of header is a new XSRF token
|
||||||
this.saveXsrfToken(error.headers.get(responseCsrfHeader));
|
this.saveXsrfToken(error.headers.get(XSRF_RESPONSE_HEADER));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Return error response as is.
|
// Return error response as is.
|
||||||
@@ -111,7 +121,7 @@ export class XsrfInterceptor implements HttpInterceptor {
|
|||||||
// Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
|
// Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
|
||||||
// This is the cookie that is parsed by Angular's tokenExtractor(),
|
// This is the cookie that is parsed by Angular's tokenExtractor(),
|
||||||
// which we will send back in the X-XSRF-TOKEN header per Angular best practices.
|
// which we will send back in the X-XSRF-TOKEN header per Angular best practices.
|
||||||
this.cookieService.remove('XSRF-TOKEN');
|
this.cookieService.remove(XSRF_COOKIE);
|
||||||
this.cookieService.set('XSRF-TOKEN', token);
|
this.cookieService.set(XSRF_COOKIE, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
$submission-relationship-thumbnail-width: 80px;
|
|
||||||
|
|
||||||
.person-thumbnail {
|
.person-thumbnail {
|
||||||
width: $submission-relationship-thumbnail-width;
|
width: var(--ds-submission-relationship-thumbnail-width);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
$submission-relationship-thumbnail-width: 80px;
|
|
||||||
|
|
||||||
.person-thumbnail {
|
.person-thumbnail {
|
||||||
width: $submission-relationship-thumbnail-width;
|
width: var(--ds-submission-relationship-thumbnail-width);
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +1,42 @@
|
|||||||
$footer-bg: $gray-100;
|
:host {
|
||||||
$footer-border: 1px solid darken($footer-bg, 10%);
|
--ds-footer-bg: var(--bs-gray-100);
|
||||||
$footer-padding: $spacer * 1.5;
|
--ds-footer-border: 1px solid var(--bs-gray-300);
|
||||||
$footer-logo-height: 55px;
|
--ds-footer-padding: calc(var(--bs-spacer) * 1.5);
|
||||||
|
--ds-footer-logo-height: 55px;
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background-color: $footer-bg;
|
background-color: var(--ds-footer-bg);
|
||||||
border-top: $footer-border;
|
border-top: var(--ds-footer-border);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: $footer-padding;
|
padding: var(--ds-footer-padding);
|
||||||
padding-bottom: $spacer;
|
padding-bottom: var(--bs-spacer);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: $footer-logo-height;
|
height: var(--ds-footer-logo-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-top: $spacer * 0.5;
|
padding-top: calc(var(--bs-spacer) * 0.5);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
a {
|
a {
|
||||||
padding: 0 $spacer/2;
|
padding: 0 calc(var(--bs-spacer) / 2);
|
||||||
color: inherit
|
color: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
border-right: 1px map-get($theme-colors, secondary) solid;
|
border-right: 1px var(--bs-secondary) solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||||
:host.open {
|
:host.open {
|
||||||
background-color: $white;
|
background-color: var(--bs-white);
|
||||||
top: 0;
|
top: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
.navbar-brand img {
|
.navbar-brand img {
|
||||||
height: $header-logo-height;
|
height: var(--ds-header-logo-height);
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
||||||
height: $header-logo-height-xs;
|
height: var(--ds-header-logo-height-xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navbar-toggler .navbar-toggler-icon {
|
.navbar-toggler .navbar-toggler-icon {
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
|
|
||||||
.navbar ::ng-deep {
|
.navbar ::ng-deep {
|
||||||
a {
|
a {
|
||||||
color: $header-icon-color;
|
color: var(--ds-header-icon-color);
|
||||||
|
|
||||||
&:hover, &focus {
|
&:hover, &focus {
|
||||||
color: darken($header-icon-color, 15%);
|
color: var(--ds-header-icon-color-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,8 @@
|
|||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
::ng-deep a.nav-link {
|
::ng-deep a.nav-link {
|
||||||
padding-right: $spacer;
|
padding-right: var(--bs-spacer);
|
||||||
padding-left: $spacer;
|
padding-left: var(--bs-spacer);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
.dropdown-toggle {
|
.dropdown-toggle {
|
||||||
&:after {
|
&:after {
|
||||||
float: right;
|
float: right;
|
||||||
margin-top: $spacer/2;
|
margin-top: calc(var(--bs-spacer) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
nav.navbar {
|
nav.navbar {
|
||||||
border-bottom: 1px $gray-400 solid;
|
border-bottom: 1px var(--bs-gray-400) solid;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ nav.navbar {
|
|||||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||||
.navbar {
|
.navbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $white;
|
background-color: var(--bs-white);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -19,8 +19,8 @@ nav.navbar {
|
|||||||
|
|
||||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||||
.reset-padding-md {
|
.reset-padding-md {
|
||||||
margin-left: -$spacer/2;
|
margin-left: calc(var(--bs-spacer) / -2);
|
||||||
margin-right: -$spacer/2;
|
margin-right: calc(var(--bs-spacer) / -2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ nav.navbar {
|
|||||||
.navbar-expand-md.navbar-container {
|
.navbar-expand-md.navbar-container {
|
||||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||||
> .container {
|
> .container {
|
||||||
padding: 0 $spacer;
|
padding: 0 var(--bs-spacer);
|
||||||
}
|
}
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user