mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into w2p-92282_Fix-missing-auth-tokens-when-retrieving-Bitstreams
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
strategy:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
node-version: [14.x, 16.x]
|
||||
# Do NOT exit immediately if one matrix job fails
|
||||
fail-fast: false
|
||||
# These are the actual CI steps to perform per job
|
||||
@@ -82,11 +82,11 @@ jobs:
|
||||
run: yarn run test:headless
|
||||
|
||||
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
|
||||
# Upload coverage reports to Codecov (for Node v12 only)
|
||||
# Upload coverage reports to Codecov (for one version of Node only)
|
||||
# https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: codecov/codecov-action@v2
|
||||
if: matrix.node-version == '12.x'
|
||||
if: matrix.node-version == '16.x'
|
||||
|
||||
# Using docker-compose start backend using CI configuration
|
||||
# and load assetstore from a cached copy
|
||||
|
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
**Ensure you're running [Node](https://nodejs.org) `v12.x`, `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||
**Ensure you're running [Node](https://nodejs.org) `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
@@ -90,7 +90,7 @@ Requirements
|
||||
------------
|
||||
|
||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||
- Ensure you're running node `v12.x`, `v14.x` or `v16.x` and yarn == `v1.x`
|
||||
- Ensure you're running node `v14.x` or `v16.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.
|
||||
|
||||
@@ -179,7 +179,7 @@ If needing to update default configurations values for production, update local
|
||||
|
||||
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
|
||||
|
||||
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
|
||||
The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
|
||||
|
||||
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
|
||||
|
||||
|
@@ -2,7 +2,8 @@
|
||||
debug: false
|
||||
|
||||
# Angular Universal server settings
|
||||
# NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
||||
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
||||
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
||||
ui:
|
||||
ssl: false
|
||||
host: localhost
|
||||
@@ -15,7 +16,8 @@ ui:
|
||||
max: 500 # limit each IP to 500 requests per windowMs
|
||||
|
||||
# The REST API server settings
|
||||
# NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||
rest:
|
||||
ssl: true
|
||||
host: api7.dspace.org
|
||||
@@ -148,6 +150,9 @@ languages:
|
||||
- code: fi
|
||||
label: Suomi
|
||||
active: true
|
||||
- code: sv
|
||||
label: Svenska
|
||||
active: true
|
||||
- code: tr
|
||||
label: Türkçe
|
||||
active: true
|
||||
@@ -246,3 +251,10 @@ bundle:
|
||||
mediaViewer:
|
||||
image: false
|
||||
video: false
|
||||
|
||||
# Whether the end user agreement is required before users use the repository.
|
||||
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
||||
# And whether the privacy statement should exist or not.
|
||||
info:
|
||||
enableEndUserAgreement: true
|
||||
enablePrivacyStatement: true
|
||||
|
@@ -1,7 +1,9 @@
|
||||
# Docker Compose files
|
||||
|
||||
***
|
||||
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
|
||||
:warning: **THESE IMAGES ARE NOT PRODUCTION READY** The below Docker Compose images/resources were built for development/testing only. Therefore, they may not be fully secured or up-to-date, and should not be used in production.
|
||||
|
||||
If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario.
|
||||
***
|
||||
|
||||
## 'Dockerfile' in root directory
|
||||
|
@@ -107,7 +107,7 @@
|
||||
"mirador": "^3.3.0",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.11.0",
|
||||
"moment": "^2.29.2",
|
||||
"moment": "^2.29.4",
|
||||
"morgan": "^1.10.0",
|
||||
"ng-mocks": "^13.1.1",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
||||
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||
[paginationOptions]="config"
|
||||
|
@@ -36,12 +36,12 @@
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||
|
||||
<ds-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-loading>
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
|
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
||||
[paginationOptions]="config"
|
||||
|
@@ -20,6 +20,8 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import createSpy = jasmine.createSpy;
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||
|
||||
describe('AdminSidebarComponent', () => {
|
||||
let comp: AdminSidebarComponent;
|
||||
@@ -60,6 +62,7 @@ describe('AdminSidebarComponent', () => {
|
||||
declarations: [AdminSidebarComponent],
|
||||
providers: [
|
||||
Injector,
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
|
@@ -9,6 +9,7 @@ import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
/**
|
||||
* Component representing the admin sidebar
|
||||
@@ -56,9 +57,10 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
public route: ActivatedRoute
|
||||
public route: ActivatedRoute,
|
||||
protected themeService: ThemeService
|
||||
) {
|
||||
super(menuService, injector, authorizationService, route);
|
||||
super(menuService, injector, authorizationService, route, themeService);
|
||||
this.inFocus$ = new BehaviorSubject(false);
|
||||
}
|
||||
|
||||
|
@@ -107,6 +107,8 @@ export function getPageInternalServerErrorRoute() {
|
||||
return `/${INTERNAL_SERVER_ERROR}`;
|
||||
}
|
||||
|
||||
export const ERROR_PAGE = 'error';
|
||||
|
||||
export const INFO_MODULE_PATH = 'info';
|
||||
export function getInfoModulePath() {
|
||||
return `/${INFO_MODULE_PATH}`;
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
ACCESS_CONTROL_MODULE_PATH,
|
||||
ADMIN_MODULE_PATH,
|
||||
BITSTREAM_MODULE_PATH,
|
||||
ERROR_PAGE,
|
||||
FORBIDDEN_PATH,
|
||||
FORGOT_PASSWORD_PATH,
|
||||
HEALTH_PAGE_PATH,
|
||||
@@ -38,11 +39,13 @@ import {
|
||||
} from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||
import { MenuResolver } from './menu.resolver';
|
||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||
{ path: ERROR_PAGE , component: ThemedPageErrorComponent },
|
||||
{
|
||||
path: '',
|
||||
canActivate: [AuthBlockingGuard],
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
||||
|
||||
@@ -49,6 +49,7 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { getDefaultThemeConfig } from '../config/config.util';
|
||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
@@ -106,6 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private localeService: LocaleService,
|
||||
private breadcrumbsService: BreadcrumbsService,
|
||||
private modalService: NgbModal,
|
||||
private modalConfig: NgbModalConfig,
|
||||
@Optional() private cookiesService: KlaroService,
|
||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||
) {
|
||||
@@ -166,6 +168,16 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
/** Implement behavior for interface {@link ModalBeforeDismiss} */
|
||||
this.modalConfig.beforeDismiss = async function () {
|
||||
if (typeof this?.componentInstance?.beforeDismiss === 'function') {
|
||||
return this.componentInstance.beforeDismiss();
|
||||
}
|
||||
|
||||
// fall back to default behavior
|
||||
return true;
|
||||
};
|
||||
|
||||
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
||||
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -40,7 +40,7 @@
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()">
|
||||
</ds-browse-by>
|
||||
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</section>
|
||||
<ng-container *ngVar="(parent$ | async) as parent">
|
||||
|
@@ -18,6 +18,8 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export const BBM_PAGINATION_ID = 'bbm';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-metadata-page',
|
||||
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||
@@ -50,7 +52,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
||||
* The pagination config used to display the values
|
||||
*/
|
||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'bbm',
|
||||
id: BBM_PAGINATION_ID,
|
||||
currentPage: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import {
|
||||
DEFAULT_THEME,
|
||||
resolveTheme
|
||||
} from '../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
|
||||
export enum BrowseByDataType {
|
||||
Title = 'title',
|
||||
@@ -10,7 +14,7 @@ export enum BrowseByDataType {
|
||||
|
||||
export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata;
|
||||
|
||||
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||
export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType, theme) => GenericConstructor<any>>('getComponentByBrowseByType', {
|
||||
providedIn: 'root',
|
||||
factory: () => getComponentByBrowseByType
|
||||
});
|
||||
@@ -20,13 +24,17 @@ const map = new Map();
|
||||
/**
|
||||
* Decorator used for rendering Browse-By pages by type
|
||||
* @param browseByType The type of page
|
||||
* @param theme The optional theme for the component
|
||||
*/
|
||||
export function rendersBrowseBy(browseByType: BrowseByDataType) {
|
||||
export function rendersBrowseBy(browseByType: BrowseByDataType, theme = DEFAULT_THEME) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(map.get(browseByType))) {
|
||||
map.set(browseByType, component);
|
||||
map.set(browseByType, new Map());
|
||||
}
|
||||
if (hasNoValue(map.get(browseByType).get(theme))) {
|
||||
map.get(browseByType).set(theme, component);
|
||||
} else {
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}"`);
|
||||
throw new Error(`There can't be more than one component to render Browse-By of type "${browseByType}" and theme "${theme}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -34,11 +42,16 @@ export function rendersBrowseBy(browseByType: BrowseByDataType) {
|
||||
/**
|
||||
* Get the component used for rendering a Browse-By page by type
|
||||
* @param browseByType The type of page
|
||||
* @param theme the theme to match
|
||||
*/
|
||||
export function getComponentByBrowseByType(browseByType) {
|
||||
const comp = map.get(browseByType);
|
||||
export function getComponentByBrowseByType(browseByType, theme) {
|
||||
let themeMap = map.get(browseByType);
|
||||
if (hasNoValue(themeMap)) {
|
||||
themeMap = map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
}
|
||||
const comp = resolveTheme(themeMap, theme);
|
||||
if (hasNoValue(comp)) {
|
||||
map.get(DEFAULT_BROWSE_BY_TYPE);
|
||||
return themeMap.get(DEFAULT_THEME);
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { BehaviorSubject, of as observableOf } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
describe('BrowseBySwitcherComponent', () => {
|
||||
let comp: BrowseBySwitcherComponent;
|
||||
@@ -44,11 +45,20 @@ describe('BrowseBySwitcherComponent', () => {
|
||||
data
|
||||
};
|
||||
|
||||
let themeService: ThemeService;
|
||||
let themeName: string;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeName = 'dspace';
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: themeName,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseBySwitcherComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -68,7 +78,7 @@ describe('BrowseBySwitcherComponent', () => {
|
||||
});
|
||||
|
||||
it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => {
|
||||
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType);
|
||||
expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.dataType, themeName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,7 @@ import { map } from 'rxjs/operators';
|
||||
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { BrowseDefinition } from '../../core/shared/browse-definition.model';
|
||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
@@ -21,7 +22,8 @@ export class BrowseBySwitcherComponent implements OnInit {
|
||||
browseByComponent: Observable<any>;
|
||||
|
||||
public constructor(protected route: ActivatedRoute,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor<any>) {
|
||||
protected themeService: ThemeService,
|
||||
@Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType, theme) => GenericConstructor<any>) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +31,7 @@ export class BrowseBySwitcherComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseByComponent = this.route.data.pipe(
|
||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType))
|
||||
map((data: { browseDefinition: BrowseDefinition }) => this.getComponentByBrowseByType(data.browseDefinition.dataType, this.themeService.getThemeName()))
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import { CreateCollectionPageComponent } from './create-collection-page/create-c
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
||||
import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver';
|
||||
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
@@ -52,7 +52,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
},
|
||||
{
|
||||
path: ITEMTEMPLATE_PATH,
|
||||
component: EditItemTemplatePageComponent,
|
||||
component: ThemedEditItemTemplatePageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
item: ItemTemplatePageResolver,
|
||||
|
@@ -56,8 +56,8 @@
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed"
|
||||
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||
message="{{'loading.recent-submissions' | translate}}"></ds-themed-loading>
|
||||
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
|
||||
{{'collection.page.browse.recent.empty' | translate}}
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||
message="{{'error.collection' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="collectionRD?.isLoading"
|
||||
message="{{'loading.collection' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="collectionRD?.isLoading"
|
||||
message="{{'loading.collection' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -8,6 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
||||
import { EditItemPageModule } from '../item-page/edit-item-page/edit-item-page.module';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
@@ -32,6 +33,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
|
||||
CreateCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
EditItemTemplatePageComponent,
|
||||
ThemedEditItemTemplatePageComponent,
|
||||
CollectionItemMapperComponent
|
||||
],
|
||||
providers: [
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<label class="form-check-label"
|
||||
for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||
</div>
|
||||
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-themed-loading>
|
||||
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@@ -3,10 +3,10 @@
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
|
||||
<ds-themed-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-themed-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||
<ds-themed-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-themed-loading>
|
||||
<ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { EditItemTemplatePageComponent } from './edit-item-template-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-item-template-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for editing the item template of a collection
|
||||
*/
|
||||
export class ThemedEditItemTemplatePageComponent extends ThemedComponent<EditItemTemplatePageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditItemTemplatePageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/collection-page/edit-item-template-page/edit-item-template-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-item-template-page.component');
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<ds-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<!-- This is the tree node template for show more node -->
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
||||
@@ -12,7 +12,7 @@
|
||||
class="btn btn-outline-primary btn-sm" role="button">
|
||||
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
||||
</a>
|
||||
<ds-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading *ngIf="node===loadingNode && dataSource.loading$ | async" class="ds-themed-loading"></ds-themed-loading>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted" cdkTreeNodePadding>
|
||||
@@ -57,7 +57,7 @@
|
||||
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<ds-loading class="ds-loading"></ds-loading>
|
||||
<ds-themed-loading class="ds-themed-loading"></ds-themed-loading>
|
||||
</div>
|
||||
</cdk-tree-node>
|
||||
<!-- This is the tree node template for leaf nodes (collections and (sub)coms without children) -->
|
||||
|
@@ -41,5 +41,5 @@
|
||||
</div>
|
||||
|
||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
|
@@ -9,5 +9,5 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-themed-loading>
|
||||
</ng-container>
|
||||
|
@@ -9,5 +9,5 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-themed-loading>
|
||||
</ng-container>
|
||||
|
@@ -4,5 +4,6 @@ export enum AuthMethodType {
|
||||
Ldap = 'ldap',
|
||||
Ip = 'ip',
|
||||
X509 = 'x509',
|
||||
Oidc = 'oidc'
|
||||
Oidc = 'oidc',
|
||||
Orcid = 'orcid'
|
||||
}
|
||||
|
@@ -34,6 +34,11 @@ export class AuthMethod {
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
case 'orcid': {
|
||||
this.authMethodType = AuthMethodType.Orcid;
|
||||
this.location = location;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
|
@@ -38,7 +38,7 @@ import { SubmissionSectionModel } from './config/models/config-submission-sectio
|
||||
import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model';
|
||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||
import { coreEffects } from './core.effects';
|
||||
import { coreReducers} from './core.reducers';
|
||||
import { coreReducers } from './core.reducers';
|
||||
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||
import { CollectionDataService } from './data/collection-data.service';
|
||||
import { CommunityDataService } from './data/community-data.service';
|
||||
@@ -132,11 +132,15 @@ import { Feature } from './shared/feature.model';
|
||||
import { Authorization } from './shared/authorization.model';
|
||||
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
||||
import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service';
|
||||
import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import {
|
||||
SiteAdministratorGuard
|
||||
} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
import { DsDynamicTypeBindRelationService } from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||
import {
|
||||
DsDynamicTypeBindRelationService
|
||||
} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
|
||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
||||
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||
@@ -169,6 +173,11 @@ import { LinkHeadService } from './services/link-head.service';
|
||||
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
||||
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
||||
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
||||
import { OrcidQueueService } from './orcid/orcid-queue.service';
|
||||
import { OrcidHistoryDataService } from './orcid/orcid-history-data.service';
|
||||
import { OrcidQueue } from './orcid/model/orcid-queue.model';
|
||||
import { OrcidHistory } from './orcid/model/orcid-history.model';
|
||||
import { OrcidAuthService } from './orcid/orcid-auth.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -296,7 +305,10 @@ const PROVIDERS = [
|
||||
GroupDataService,
|
||||
FeedbackDataService,
|
||||
ResearcherProfileService,
|
||||
ProfileClaimService
|
||||
ProfileClaimService,
|
||||
OrcidAuthService,
|
||||
OrcidQueueService,
|
||||
OrcidHistoryDataService,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -358,7 +370,10 @@ export const models =
|
||||
SearchConfig,
|
||||
SubmissionAccessesModel,
|
||||
AccessStatusObject,
|
||||
ResearcherProfile
|
||||
ResearcherProfile,
|
||||
OrcidQueue,
|
||||
OrcidHistory,
|
||||
AccessStatusObject
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -894,6 +894,26 @@ describe('DataService', () => {
|
||||
expectObservable(done$).toBe('------(t|)', BOOLEAN);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only fire for the current state of the object (instead of tracking it)', () => {
|
||||
testScheduler.run(({ cold, flush }) => {
|
||||
getByHrefSpy.and.returnValue(cold('a---b---c---', {
|
||||
a: { requestUUIDs: ['request1'] }, // this is the state at the moment we're invalidating the cache
|
||||
b: { requestUUIDs: ['request2'] }, // we shouldn't keep tracking the state
|
||||
c: { requestUUIDs: ['request3'] }, // because we may invalidate when we shouldn't
|
||||
}));
|
||||
|
||||
service.invalidateByHref('some-href');
|
||||
flush();
|
||||
|
||||
// requests from the first state are marked as stale
|
||||
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
|
||||
|
||||
// request from subsequent states are ignored
|
||||
expect(requestService.setStaleByUUID).not.toHaveBeenCalledWith('request2');
|
||||
expect(requestService.setStaleByUUID).not.toHaveBeenCalledWith('request3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
@@ -595,6 +595,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
const done$ = new AsyncSubject<boolean>();
|
||||
|
||||
this.objectCache.getByHref(href).pipe(
|
||||
take(1),
|
||||
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
|
||||
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
|
||||
toArray(),
|
||||
|
@@ -19,7 +19,7 @@ export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
|
||||
|
||||
/**
|
||||
* True when user has authorization rights for the feature and object provided
|
||||
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
||||
* Redirect the user to the unauthorized page when they are not authorized for the given feature
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||
|
@@ -29,4 +29,5 @@ export enum FeatureID {
|
||||
CanViewUsageStatistics = 'canViewUsageStatistics',
|
||||
CanSendFeedback = 'canSendFeedback',
|
||||
CanClaimItem = 'canClaimItem',
|
||||
CanSynchronizeWithORCID = 'canSynchronizeWithORCID'
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ export class ProcessDataService extends DataService<Process> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for a process his files
|
||||
* Get the endpoint for the files of the process
|
||||
* @param processId The ID of the process
|
||||
*/
|
||||
getFilesEndpoint(processId: string): Observable<string> {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
/**
|
||||
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
|
||||
@@ -18,6 +19,9 @@ export abstract class AbstractEndUserAgreementGuard implements CanActivate {
|
||||
* when they're finished accepting the agreement
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
if (!environment.info.enableEndUserAgreement) {
|
||||
return observableOf(true);
|
||||
}
|
||||
return this.hasAccepted().pipe(
|
||||
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
|
||||
);
|
||||
|
@@ -2,6 +2,7 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-u
|
||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||
import { Router, UrlTree } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment.test';
|
||||
|
||||
describe('EndUserAgreementGuard', () => {
|
||||
let guard: EndUserAgreementCurrentUserGuard;
|
||||
@@ -44,5 +45,24 @@ describe('EndUserAgreementGuard', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the end user agreement is disabled', () => {
|
||||
it('should return true', (done) => {
|
||||
environment.info.enableEndUserAgreement = false;
|
||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||
console.log(result);
|
||||
expect(result).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not resolve to the end user agreement page', (done) => {
|
||||
environment.info.enableEndUserAgreement = false;
|
||||
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
/**
|
||||
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
|
||||
@@ -19,6 +20,10 @@ export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGu
|
||||
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
|
||||
*/
|
||||
hasAccepted(): Observable<boolean> {
|
||||
if (!environment.info.enableEndUserAgreement) {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
||||
}
|
||||
|
||||
|
@@ -55,7 +55,7 @@ export class EndUserAgreementService {
|
||||
|
||||
/**
|
||||
* Set the current user's accepted agreement status
|
||||
* When a user is authenticated, set his/her metadata to the provided value
|
||||
* When a user is authenticated, set their metadata to the provided value
|
||||
* When no user is authenticated, set the cookie to the provided value
|
||||
* @param accepted
|
||||
*/
|
||||
|
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
89
src/app/core/orcid/model/orcid-history.model.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../../cache/builders/build-decorators';
|
||||
import { HALLink } from '../../shared/hal-link.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
import { ORCID_HISTORY } from './orcid-history.resource-type';
|
||||
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||
|
||||
/**
|
||||
* Class the represents a Orcid History.
|
||||
*/
|
||||
@typedObject
|
||||
export class OrcidHistory extends CacheableObject {
|
||||
|
||||
static type = ORCID_HISTORY;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* The identifier of this Orcid History record
|
||||
*/
|
||||
@autoserialize
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The name of the related entity
|
||||
*/
|
||||
@autoserialize
|
||||
entityName: string;
|
||||
|
||||
/**
|
||||
* The identifier of the profileItem of this Orcid History record.
|
||||
*/
|
||||
@autoserialize
|
||||
profileItemId: string;
|
||||
|
||||
/**
|
||||
* The identifier of the entity related to this Orcid History record.
|
||||
*/
|
||||
@autoserialize
|
||||
entityId: string;
|
||||
|
||||
/**
|
||||
* The type of the entity related to this Orcid History record.
|
||||
*/
|
||||
@autoserialize
|
||||
entityType: string;
|
||||
|
||||
/**
|
||||
* The response status coming from ORCID api.
|
||||
*/
|
||||
@autoserialize
|
||||
status: number;
|
||||
|
||||
/**
|
||||
* The putCode assigned by ORCID to the entity.
|
||||
*/
|
||||
@autoserialize
|
||||
putCode: string;
|
||||
|
||||
/**
|
||||
* The last send attempt timestamp.
|
||||
*/
|
||||
lastAttempt: string;
|
||||
|
||||
/**
|
||||
* The success send attempt timestamp.
|
||||
*/
|
||||
successAttempt: string;
|
||||
|
||||
/**
|
||||
* The response coming from ORCID.
|
||||
*/
|
||||
responseMessage: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this Orcid History record
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink,
|
||||
};
|
||||
|
||||
}
|
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-history.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for OrcidHistory
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ORCID_HISTORY = new ResourceType('orcidhistory');
|
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
68
src/app/core/orcid/model/orcid-queue.model.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { typedObject } from '../../cache/builders/build-decorators';
|
||||
import { HALLink } from '../../shared/hal-link.model';
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
import { ORCID_QUEUE } from './orcid-queue.resource-type';
|
||||
import { CacheableObject } from '../../cache/cacheable-object.model';
|
||||
|
||||
/**
|
||||
* Class the represents a Orcid Queue.
|
||||
*/
|
||||
@typedObject
|
||||
export class OrcidQueue extends CacheableObject {
|
||||
|
||||
static type = ORCID_QUEUE;
|
||||
|
||||
/**
|
||||
* The object type
|
||||
*/
|
||||
@excludeFromEquals
|
||||
@autoserialize
|
||||
type: ResourceType;
|
||||
|
||||
/**
|
||||
* The identifier of this Orcid Queue record
|
||||
*/
|
||||
@autoserialize
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The record description.
|
||||
*/
|
||||
@autoserialize
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* The identifier of the profileItem of this Orcid Queue record.
|
||||
*/
|
||||
@autoserialize
|
||||
profileItemId: string;
|
||||
|
||||
/**
|
||||
* The identifier of the entity related to this Orcid Queue record.
|
||||
*/
|
||||
@autoserialize
|
||||
entityId: string;
|
||||
|
||||
/**
|
||||
* The type of this Orcid Queue record.
|
||||
*/
|
||||
@autoserialize
|
||||
recordType: string;
|
||||
|
||||
/**
|
||||
* The operation related to this Orcid Queue record.
|
||||
*/
|
||||
@autoserialize
|
||||
operation: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this Orcid Queue record
|
||||
*/
|
||||
@deserialize
|
||||
_links: {
|
||||
self: HALLink,
|
||||
};
|
||||
|
||||
}
|
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
9
src/app/core/orcid/model/orcid-queue.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from '../../shared/resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for OrcidQueue
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const ORCID_QUEUE = new ResourceType('orcidqueue');
|
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
329
src/app/core/orcid/orcid-auth.service.spec.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { NativeWindowRefMock } from '../../shared/mocks/mock-native-window-ref';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { OrcidAuthService } from './orcid-auth.service';
|
||||
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||
|
||||
describe('OrcidAuthService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: OrcidAuthService;
|
||||
let serviceAsAny: any;
|
||||
|
||||
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>;
|
||||
let configurationDataService: ConfigurationDataService;
|
||||
let nativeWindowService: NativeWindowRefMock;
|
||||
let routerStub: any;
|
||||
|
||||
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
|
||||
const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||
id: researcherProfileId,
|
||||
visible: false,
|
||||
type: 'profile',
|
||||
_links: {
|
||||
item: {
|
||||
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||
},
|
||||
self: {
|
||||
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||
id: researcherProfileId,
|
||||
visible: true,
|
||||
type: 'profile',
|
||||
_links: {
|
||||
item: {
|
||||
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
|
||||
},
|
||||
self: {
|
||||
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
id: 'mockItemUnlinkedToOrcid',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}],
|
||||
'dspace.object.owner': [{
|
||||
'value': 'test person',
|
||||
'language': null,
|
||||
'authority': 'researcher-profile-id',
|
||||
'confidence': 600,
|
||||
'place': 0
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}],
|
||||
'dspace.object.owner': [{
|
||||
'value': 'test person',
|
||||
'language': null,
|
||||
'authority': 'researcher-profile-id',
|
||||
'confidence': 600,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.authenticated': [{
|
||||
'value': '2022-06-10T15:15:12.952872',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.scope': [{
|
||||
'value': '/authenticate',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}, {
|
||||
'value': '/read-limited',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 1
|
||||
}, {
|
||||
'value': '/activities/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 2
|
||||
}, {
|
||||
'value': '/person/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 3
|
||||
}],
|
||||
'person.identifier.orcid': [{
|
||||
'value': 'orcid-id',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
const disconnectionAllowAdmin = {
|
||||
uuid: 'orcid.disconnection.allowed-users',
|
||||
name: 'orcid.disconnection.allowed-users',
|
||||
values: ['only_admin']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
const disconnectionAllowAdminOwner = {
|
||||
uuid: 'orcid.disconnection.allowed-users',
|
||||
name: 'orcid.disconnection.allowed-users',
|
||||
values: ['admin_and_owner']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
const authorizeUrl = {
|
||||
uuid: 'orcid.authorize-url',
|
||||
name: 'orcid.authorize-url',
|
||||
values: ['orcid.authorize-url']
|
||||
} as ConfigurationProperty;
|
||||
const appClientId = {
|
||||
uuid: 'orcid.application-client-id',
|
||||
name: 'orcid.application-client-id',
|
||||
values: ['orcid.application-client-id']
|
||||
} as ConfigurationProperty;
|
||||
const orcidScope = {
|
||||
uuid: 'orcid.scope',
|
||||
name: 'orcid.scope',
|
||||
values: ['/authenticate', '/read-limited']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
routerStub = new RouterMock();
|
||||
researcherProfileService = jasmine.createSpyObj('ResearcherProfileService', {
|
||||
findById: jasmine.createSpy('findById'),
|
||||
updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations')
|
||||
});
|
||||
configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||
findByPropertyName: jasmine.createSpy('findByPropertyName')
|
||||
});
|
||||
nativeWindowService = new NativeWindowRefMock();
|
||||
|
||||
service = new OrcidAuthService(
|
||||
nativeWindowService,
|
||||
configurationDataService,
|
||||
researcherProfileService,
|
||||
routerStub);
|
||||
|
||||
serviceAsAny = service;
|
||||
});
|
||||
|
||||
|
||||
describe('isLinkedToOrcid', () => {
|
||||
it('should return true when item has metadata', () => {
|
||||
const result = service.isLinkedToOrcid(mockItemLinkedToOrcid);
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('should return true when item has no metadata', () => {
|
||||
const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onlyAdminCanDisconnectProfileFromOrcid', () => {
|
||||
it('should return true when property is only_admin', () => {
|
||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin));
|
||||
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||
const expected = cold('(a|)', {
|
||||
a: true
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return false on faild', () => {
|
||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||
const result = service.onlyAdminCanDisconnectProfileFromOrcid();
|
||||
const expected = cold('(a|)', {
|
||||
a: false
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ownerCanDisconnectProfileFromOrcid', () => {
|
||||
it('should return true when property is admin_and_owner', () => {
|
||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner));
|
||||
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||
const expected = cold('(a|)', {
|
||||
a: true
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
|
||||
it('should return false on faild', () => {
|
||||
spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$());
|
||||
const result = service.ownerCanDisconnectProfileFromOrcid();
|
||||
const expected = cold('(a|)', {
|
||||
a: false
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkOrcidByItem', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||
});
|
||||
|
||||
it('should call updateByOrcidOperations method properly', () => {
|
||||
const operations: AddOperation<string>[] = [{
|
||||
path: '/orcid',
|
||||
op: 'add',
|
||||
value: 'test-code'
|
||||
}];
|
||||
|
||||
scheduler.schedule(() => service.linkOrcidByItem(mockItemUnlinkedToOrcid, 'test-code').subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unlinkOrcidByItem', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
researcherProfileService.findById.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
|
||||
});
|
||||
|
||||
it('should call updateByOrcidOperations method properly', () => {
|
||||
const operations: RemoveOperation[] = [{
|
||||
path: '/orcid',
|
||||
op: 'remove'
|
||||
}];
|
||||
|
||||
scheduler.schedule(() => service.unlinkOrcidByItem(mockItemLinkedToOrcid).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(researcherProfile, operations);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrcidAuthorizeUrl', () => {
|
||||
beforeEach(() => {
|
||||
routerStub.setRoute('/entities/person/uuid/orcid');
|
||||
(service as any).configurationService.findByPropertyName.and.returnValues(
|
||||
createSuccessfulRemoteDataObject$(authorizeUrl),
|
||||
createSuccessfulRemoteDataObject$(appClientId),
|
||||
createSuccessfulRemoteDataObject$(orcidScope)
|
||||
);
|
||||
});
|
||||
|
||||
it('should build the url properly', () => {
|
||||
const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid);
|
||||
const redirectUri: string = new URLCombiner(nativeWindowService.nativeWindow.origin, encodeURIComponent(routerStub.url.split('?')[0])).toString();
|
||||
const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited';
|
||||
|
||||
const expected = cold('(a|)', {
|
||||
a: url
|
||||
});
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrcidAuthorizationScopesByItem', () => {
|
||||
it('should return list of scopes saved in the item', () => {
|
||||
const orcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited',
|
||||
'/activities/update',
|
||||
'/person/update'
|
||||
];
|
||||
const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid);
|
||||
expect(result).toEqual(orcidScopes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOrcidAuthorizationScopes', () => {
|
||||
it('should return list of scopes by configuration', () => {
|
||||
(service as any).configurationService.findByPropertyName.and.returnValue(
|
||||
createSuccessfulRemoteDataObject$(orcidScope)
|
||||
);
|
||||
const orcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited'
|
||||
];
|
||||
const expected = cold('(a|)', {
|
||||
a: orcidScopes
|
||||
});
|
||||
const result = service.getOrcidAuthorizationScopes();
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
145
src/app/core/orcid/orcid-auth.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { AddOperation, RemoveOperation } from 'fast-json-patch';
|
||||
|
||||
import { ResearcherProfileService } from '../profile/researcher-profile.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||
import { ResearcherProfile } from '../profile/model/researcher-profile.model';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||
|
||||
@Injectable()
|
||||
export class OrcidAuthService {
|
||||
|
||||
constructor(
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private configurationService: ConfigurationDataService,
|
||||
private researcherProfileService: ResearcherProfileService,
|
||||
private router: Router) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given item is linked to an ORCID profile.
|
||||
*
|
||||
* @param item the item to check
|
||||
* @returns the check result
|
||||
*/
|
||||
public isLinkedToOrcid(item: Item): boolean {
|
||||
return item.hasMetadata('dspace.orcid.authenticated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if only the admin users can disconnect a researcher profile from ORCID.
|
||||
*
|
||||
* @returns the check result
|
||||
*/
|
||||
public onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||
return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the profile's owner can disconnect that profile from ORCID.
|
||||
*
|
||||
* @returns the check result
|
||||
*/
|
||||
public ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||
map((propertyRD: RemoteData<ConfigurationProperty>) => {
|
||||
return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a link operation to ORCID profile.
|
||||
*
|
||||
* @param person The person item related to the researcher profile
|
||||
* @param code The auth-code received from orcid
|
||||
*/
|
||||
public linkOrcidByItem(person: Item, code: string): Observable<RemoteData<ResearcherProfile>> {
|
||||
const operations: AddOperation<string>[] = [{
|
||||
path: '/orcid',
|
||||
op: 'add',
|
||||
value: code
|
||||
}];
|
||||
|
||||
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform unlink operation from ORCID profile.
|
||||
*
|
||||
* @param person The person item related to the researcher profile
|
||||
*/
|
||||
public unlinkOrcidByItem(person: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||
const operations: RemoveOperation[] = [{
|
||||
path:'/orcid',
|
||||
op:'remove'
|
||||
}];
|
||||
|
||||
return this.researcherProfileService.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((profileRD) => this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and return the url to authenticate with orcid
|
||||
*
|
||||
* @param profile
|
||||
*/
|
||||
public getOrcidAuthorizeUrl(profile: Item): Observable<string> {
|
||||
return combineLatest([
|
||||
this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()),
|
||||
this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()),
|
||||
this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())]
|
||||
).pipe(
|
||||
map(([authorizeUrl, clientId, scopes]) => {
|
||||
const redirectUri = new URLCombiner(this._window.nativeWindow.origin, encodeURIComponent(this.router.url.split('?')[0]));
|
||||
console.log(redirectUri.toString());
|
||||
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||
+ scopes.values.join(' ');
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all orcid authorization scopes saved in the given item
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
public getOrcidAuthorizationScopesByItem(item: Item): string[] {
|
||||
return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all orcid authorization scopes available by configuration
|
||||
*/
|
||||
public getOrcidAuthorizationScopes(): Observable<string[]> {
|
||||
return this.configurationService.findByPropertyName('orcid.scope').pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((propertyRD: RemoteData<ConfigurationProperty>) => propertyRD.hasSucceeded ? propertyRD.payload.values : [])
|
||||
);
|
||||
}
|
||||
|
||||
private getOrcidDisconnectionAllowedUsersConfiguration(): Observable<RemoteData<ConfigurationProperty>> {
|
||||
return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
126
src/app/core/orcid/orcid-history-data.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DataService } from '../data/data.service';
|
||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||
import { ItemDataService } from '../data/item-data.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { OrcidHistory } from './model/orcid-history.model';
|
||||
import { ORCID_HISTORY } from './model/orcid-history.resource-type';
|
||||
import { OrcidQueue } from './model/orcid-queue.model';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { RestRequest } from '../data/rest-request.model';
|
||||
import { sendRequest } from '../shared/request.operators';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { FindListOptions } from '../data/find-list-options.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
class OrcidHistoryServiceImpl extends DataService<OrcidHistory> {
|
||||
public linkPath = 'orcidhistories';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<OrcidHistory>) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with Orcid History endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(ORCID_HISTORY)
|
||||
export class OrcidHistoryDataService {
|
||||
|
||||
dataService: OrcidHistoryServiceImpl;
|
||||
|
||||
responseMsToLive: number = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<OrcidHistory>,
|
||||
protected itemService: ItemDataService ) {
|
||||
|
||||
this.dataService = new OrcidHistoryServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||
notificationsService, http, comparator);
|
||||
|
||||
}
|
||||
|
||||
sendToORCID(orcidQueue: OrcidQueue): Observable<RemoteData<OrcidHistory>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
return this.getEndpoint().pipe(
|
||||
map((endpointURL: string) => {
|
||||
const options: HttpOptions = Object.create({});
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.append('Content-Type', 'text/uri-list');
|
||||
options.headers = headers;
|
||||
return new PostRequest(requestId, endpointURL, orcidQueue._links.self.href, options);
|
||||
}),
|
||||
sendRequest(this.requestService),
|
||||
switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable<RemoteData<OrcidHistory>>)
|
||||
);
|
||||
}
|
||||
|
||||
getEndpoint(): Observable<string> {
|
||||
return this.halService.getEndpoint(this.dataService.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of
|
||||
* {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object
|
||||
* @param id ID of object we want to retrieve
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||
* {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<OrcidHistory>> {
|
||||
return this.dataService.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of observables of {@link RemoteData} of {@link OrcidHistory}s, based on an href, with a list of {@link FollowLinkConfig},
|
||||
* to automatically resolve {@link HALLink}s of the {@link OrcidHistory}
|
||||
* @param href The url of object we want to retrieve
|
||||
* @param findListOptions Find list options object
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
|
||||
* {@link HALLink}s should be automatically resolved
|
||||
*/
|
||||
findAllByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<OrcidHistory>[]): Observable<RemoteData<PaginatedList<OrcidHistory>>> {
|
||||
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||
}
|
||||
|
||||
}
|
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
110
src/app/core/orcid/orcid-queue.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// eslint-disable-next-line max-classes-per-file
|
||||
import { DataService } from '../data/data.service';
|
||||
import { OrcidQueue } from './model/orcid-queue.model';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { ORCID_QUEUE } from './model/orcid-queue.resource-type';
|
||||
import { ItemDataService } from '../data/item-data.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CoreState } from '../core-state.model';
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
*/
|
||||
class OrcidQueueServiceImpl extends DataService<OrcidQueue> {
|
||||
public linkPath = 'orcidqueues';
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<OrcidQueue>) {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with Orcid Queue endpoint.
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(ORCID_QUEUE)
|
||||
export class OrcidQueueService {
|
||||
|
||||
dataService: OrcidQueueServiceImpl;
|
||||
|
||||
responseMsToLive: number = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<OrcidQueue>,
|
||||
protected configurationService: ConfigurationDataService,
|
||||
protected router: Router,
|
||||
protected itemService: ItemDataService ) {
|
||||
|
||||
this.dataService = new OrcidQueueServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||
notificationsService, http, comparator);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param itemId It represent an Id of profileItem
|
||||
* @param paginationOptions The pagination options object
|
||||
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
|
||||
* no valid cached version. Defaults to true
|
||||
* @param reRequestOnStale Whether or not the request should automatically be re-
|
||||
* requested after the response becomes stale
|
||||
* @returns { OrcidQueue }
|
||||
*/
|
||||
searchByProfileItemId(itemId: string, paginationOptions: PaginationComponentOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
|
||||
return this.dataService.searchBy('findByProfileItem', {
|
||||
searchParams: [new RequestParam('profileItemId', itemId)],
|
||||
elementsPerPage: paginationOptions.pageSize,
|
||||
currentPage: paginationOptions.currentPage
|
||||
},
|
||||
useCachedVersionIfAvailable,
|
||||
reRequestOnStale
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param orcidQueueId represents a id of orcid queue
|
||||
* @returns { NoContent }
|
||||
*/
|
||||
deleteById(orcidQueueId: number): Observable<RemoteData<NoContent>> {
|
||||
return this.dataService.delete(orcidQueueId.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will set linkPath to stale
|
||||
*/
|
||||
clearFindByProfileItemRequests() {
|
||||
this.requestService.setStaleByHrefSubstring(this.dataService.linkPath + '/search/findByProfileItem');
|
||||
}
|
||||
|
||||
}
|
@@ -12,7 +12,7 @@ describe('PaginationService', () => {
|
||||
let routeService;
|
||||
|
||||
const defaultPagination = new PaginationComponentOptions();
|
||||
const defaultSort = new SortOptions('id', SortDirection.DESC);
|
||||
const defaultSort = new SortOptions('dc.title', SortDirection.ASC);
|
||||
const defaultFindListOptions = new FindListOptions();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,7 +39,6 @@ describe('PaginationService', () => {
|
||||
service = new PaginationService(routeService, router);
|
||||
});
|
||||
|
||||
|
||||
describe('getCurrentPagination', () => {
|
||||
it('should retrieve the current pagination info from the routerService', () => {
|
||||
service.getCurrentPagination('test-id', defaultPagination).subscribe((currentPagination) => {
|
||||
@@ -56,6 +55,26 @@ describe('PaginationService', () => {
|
||||
expect(currentSort).toEqual(Object.assign(new SortOptions('score', SortDirection.ASC )));
|
||||
});
|
||||
});
|
||||
it('should return default sort when no sort specified', () => {
|
||||
// This is same as routeService (defined above), but returns no sort field or direction
|
||||
routeService = {
|
||||
getQueryParameterValue: (param) => {
|
||||
let value;
|
||||
if (param.endsWith('.page')) {
|
||||
value = 5;
|
||||
}
|
||||
if (param.endsWith('.rpp')) {
|
||||
value = 10;
|
||||
}
|
||||
return observableOf(value);
|
||||
}
|
||||
};
|
||||
service = new PaginationService(routeService, router);
|
||||
|
||||
service.getCurrentSort('test-id', defaultSort).subscribe((currentSort) => {
|
||||
expect(currentSort).toEqual(defaultSort);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('getFindListOptions', () => {
|
||||
it('should retrieve the current findListOptions info from the routerService', () => {
|
||||
|
@@ -24,7 +24,11 @@ import { isNumeric } from '../../shared/numeric.util';
|
||||
*/
|
||||
export class PaginationService {
|
||||
|
||||
private defaultSortOptions = new SortOptions('id', SortDirection.ASC);
|
||||
/**
|
||||
* Sort on title ASC by default
|
||||
* @type {SortOptions}
|
||||
*/
|
||||
private defaultSortOptions = new SortOptions('dc.title', SortDirection.ASC);
|
||||
|
||||
private clearParams = {};
|
||||
|
||||
|
@@ -26,6 +26,8 @@ import { ReplaceOperation } from 'fast-json-patch';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
|
||||
describe('ResearcherProfileService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
@@ -36,6 +38,7 @@ describe('ResearcherProfileService', () => {
|
||||
let objectCache: ObjectCacheService;
|
||||
let halService: HALEndpointService;
|
||||
let responseCacheEntry: RequestEntry;
|
||||
let routerStub: any;
|
||||
|
||||
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
|
||||
@@ -86,6 +89,113 @@ describe('ResearcherProfileService', () => {
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
id: 'mockItemUnlinkedToOrcid',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}],
|
||||
'dspace.object.owner': [{
|
||||
'value': 'test person',
|
||||
'language': null,
|
||||
'authority': 'researcher-profile-id',
|
||||
'confidence': 600,
|
||||
'place': 0
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}],
|
||||
'dspace.object.owner': [{
|
||||
'value': 'test person',
|
||||
'language': null,
|
||||
'authority': 'researcher-profile-id',
|
||||
'confidence': 600,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.authenticated': [{
|
||||
'value': '2022-06-10T15:15:12.952872',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.scope': [{
|
||||
'value': '/authenticate',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}, {
|
||||
'value': '/read-limited',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 1
|
||||
}, {
|
||||
'value': '/activities/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 2
|
||||
}, {
|
||||
'value': '/person/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 3
|
||||
}],
|
||||
'person.identifier.orcid': [{
|
||||
'value': 'orcid-id',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
const disconnectionAllowAdmin = {
|
||||
uuid: 'orcid.disconnection.allowed-users',
|
||||
name: 'orcid.disconnection.allowed-users',
|
||||
values: ['only_admin']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
const disconnectionAllowAdminOwner = {
|
||||
uuid: 'orcid.disconnection.allowed-users',
|
||||
name: 'orcid.disconnection.allowed-users',
|
||||
values: ['admin_and_owner']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
const authorizeUrl = {
|
||||
uuid: 'orcid.authorize-url',
|
||||
name: 'orcid.authorize-url',
|
||||
values: ['orcid.authorize-url']
|
||||
} as ConfigurationProperty;
|
||||
const appClientId = {
|
||||
uuid: 'orcid.application-client-id',
|
||||
name: 'orcid.application-client-id',
|
||||
values: ['orcid.application-client-id']
|
||||
} as ConfigurationProperty;
|
||||
const orcidScope = {
|
||||
uuid: 'orcid.scope',
|
||||
name: 'orcid.scope',
|
||||
values: ['/authenticate', '/read-limited']
|
||||
} as ConfigurationProperty;
|
||||
|
||||
const endpointURL = `https://rest.api/rest/api/profiles`;
|
||||
const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item';
|
||||
const sourceUri = `https://rest.api/rest/api/external-source/profile`;
|
||||
@@ -132,7 +242,7 @@ describe('ResearcherProfileService', () => {
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const routerStub: any = new RouterMock();
|
||||
routerStub = new RouterMock();
|
||||
const itemService = jasmine.createSpyObj('ItemService', {
|
||||
findByHref: jasmine.createSpy('findByHref')
|
||||
});
|
||||
@@ -271,7 +381,7 @@ describe('ResearcherProfileService', () => {
|
||||
});
|
||||
|
||||
describe('createFromExternalSource', () => {
|
||||
let patchSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
@@ -293,4 +403,17 @@ describe('ResearcherProfileService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateByOrcidOperations', () => {
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
|
||||
});
|
||||
|
||||
it('should call patch method properly', () => {
|
||||
scheduler.schedule(() => service.updateByOrcidOperations(researcherProfile, []).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -4,10 +4,9 @@ import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ReplaceOperation } from 'fast-json-patch';
|
||||
import { Operation, ReplaceOperation } from 'fast-json-patch';
|
||||
import { Observable } from 'rxjs';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -24,10 +23,11 @@ import { ResearcherProfile } from './model/researcher-profile.model';
|
||||
import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
|
||||
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
||||
import { PostRequest } from '../data/request.models';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty } from '../../shared/empty.util';
|
||||
import { CoreState } from '../core-state.model';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
|
||||
/**
|
||||
* A private DataService implementation to delegate specific methods to.
|
||||
@@ -56,9 +56,9 @@ class ResearcherProfileServiceImpl extends DataService<ResearcherProfile> {
|
||||
@dataService(RESEARCHER_PROFILE)
|
||||
export class ResearcherProfileService {
|
||||
|
||||
dataService: ResearcherProfileServiceImpl;
|
||||
protected dataService: ResearcherProfileServiceImpl;
|
||||
|
||||
responseMsToLive: number = 10 * 1000;
|
||||
protected responseMsToLive: number = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected requestService: RequestService,
|
||||
@@ -112,6 +112,20 @@ export class ResearcherProfileService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a researcher profile by its own related item
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
public findByRelatedItem(item: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||
const profileId = item.firstMetadata('dspace.object.owner')?.authority;
|
||||
if (isEmpty(profileId)) {
|
||||
return createFailedRemoteDataObject$();
|
||||
} else {
|
||||
return this.findById(profileId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the item id related to the given researcher profile.
|
||||
*
|
||||
@@ -164,4 +178,17 @@ export class ResearcherProfileService {
|
||||
|
||||
return this.rdbService.buildFromRequestUUID(requestId, followLink('item'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update researcher profile by patch orcid operation
|
||||
*
|
||||
* @param researcherProfile
|
||||
* @param operations
|
||||
*/
|
||||
public updateByOrcidOperations(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
|
||||
return this.dataService.patch(researcherProfile, operations);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ export class MetadataValue implements MetadataValueInterface {
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The place of this MetadataValue within his list of metadata
|
||||
* The place of this MetadataValue within its list of metadata
|
||||
* This is used to render metadata in a specific custom order
|
||||
*/
|
||||
@autoserialize
|
||||
@@ -105,7 +105,7 @@ export class MetadatumViewModel {
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The place of this MetadataValue within his list of metadata
|
||||
* The place of this MetadataValue within its list of metadata
|
||||
* This is used to render metadata in a specific custom order
|
||||
*/
|
||||
place: number;
|
||||
|
@@ -15,6 +15,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { ConfigurationDataService } from '../core/data/configuration-data.service';
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
||||
import { HandleService } from '../shared/handle.service';
|
||||
|
||||
describe('CurationFormComponent', () => {
|
||||
let comp: CurationFormComponent;
|
||||
@@ -23,6 +24,7 @@ describe('CurationFormComponent', () => {
|
||||
let scriptDataService: ScriptDataService;
|
||||
let processDataService: ProcessDataService;
|
||||
let configurationDataService: ConfigurationDataService;
|
||||
let handleService: HandleService;
|
||||
let notificationsService;
|
||||
let router;
|
||||
|
||||
@@ -51,6 +53,10 @@ describe('CurationFormComponent', () => {
|
||||
}))
|
||||
});
|
||||
|
||||
handleService = {
|
||||
normalizeHandle: (a) => a
|
||||
} as any;
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
router = new RouterStub();
|
||||
|
||||
@@ -58,11 +64,12 @@ describe('CurationFormComponent', () => {
|
||||
imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
|
||||
declarations: [CurationFormComponent],
|
||||
providers: [
|
||||
{provide: ScriptDataService, useValue: scriptDataService},
|
||||
{provide: ProcessDataService, useValue: processDataService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: ConfigurationDataService, useValue: configurationDataService},
|
||||
{ provide: ScriptDataService, useValue: scriptDataService },
|
||||
{ provide: ProcessDataService, useValue: processDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: HandleService, useValue: handleService },
|
||||
{ provide: Router, useValue: router},
|
||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -143,4 +150,13 @@ describe('CurationFormComponent', () => {
|
||||
{name: '-i', value: 'all'},
|
||||
], []);
|
||||
});
|
||||
|
||||
it(`should show an error notification and return when an invalid dsoHandle is provided`, () => {
|
||||
comp.dsoHandle = 'test-handle';
|
||||
spyOn(handleService, 'normalizeHandle').and.returnValue(null);
|
||||
comp.submit();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
expect(scriptDataService.invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../shared/empty.util';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { Router } from '@angular/router';
|
||||
import { ProcessDataService } from '../core/data/processes/process-data.service';
|
||||
@@ -14,9 +14,9 @@ import { ConfigurationDataService } from '../core/data/configuration-data.servic
|
||||
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
|
||||
import { HandleService } from '../shared/handle.service';
|
||||
|
||||
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
|
||||
|
||||
/**
|
||||
* Component responsible for rendering the Curation Task form
|
||||
*/
|
||||
@@ -39,6 +39,7 @@ export class CurationFormComponent implements OnInit {
|
||||
private processDataService: ProcessDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private handleService: HandleService,
|
||||
private router: Router
|
||||
) {
|
||||
}
|
||||
@@ -76,13 +77,19 @@ export class CurationFormComponent implements OnInit {
|
||||
const taskName = this.form.get('task').value;
|
||||
let handle;
|
||||
if (this.hasHandleValue()) {
|
||||
handle = this.dsoHandle;
|
||||
handle = this.handleService.normalizeHandle(this.dsoHandle);
|
||||
if (isEmpty(handle)) {
|
||||
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
|
||||
this.translateService.get('curation.form.submit.error.invalid-handle'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
handle = this.form.get('handle').value;
|
||||
handle = this.handleService.normalizeHandle(this.form.get('handle').value);
|
||||
if (isEmpty(handle)) {
|
||||
handle = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
this.scriptDataService.invoke('curate', [
|
||||
{ name: '-t', value: taskName },
|
||||
{ name: '-i', value: handle },
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{{'journalissue.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalissue.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
|
||||
@listableObjectComponent('JournalIssue', ViewMode.StandalonePage)
|
||||
@Component({
|
||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Journal Issue
|
||||
*/
|
||||
export class JournalIssueComponent extends ItemComponent {
|
||||
export class JournalIssueComponent extends VersionedItemComponent {
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{{'journalvolume.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journalvolume.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
|
||||
@listableObjectComponent('JournalVolume', ViewMode.StandalonePage)
|
||||
@Component({
|
||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Journal Volume
|
||||
*/
|
||||
export class JournalVolumeComponent extends ItemComponent {
|
||||
export class JournalVolumeComponent extends VersionedItemComponent {
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{{'journal.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'journal.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +47,8 @@
|
||||
<ds-tabbed-related-entities-search [item]="object"
|
||||
[relationTypes]="[{
|
||||
label: 'isJournalOfPublication',
|
||||
filter: 'isJournalOfPublication'
|
||||
filter: 'isJournalOfPublication',
|
||||
configuration: 'default-relationships'
|
||||
}]">
|
||||
</ds-tabbed-related-entities-search>
|
||||
</div>
|
||||
|
@@ -29,6 +29,11 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { JournalComponent } from './journal.component';
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
|
||||
import { VersionDataService } from '../../../../core/data/version-data.service';
|
||||
import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
|
||||
let comp: JournalComponent;
|
||||
let fixture: ComponentFixture<JournalComponent>;
|
||||
@@ -65,12 +70,15 @@ describe('JournalComponent', () => {
|
||||
};
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
}),
|
||||
RouterTestingModule,
|
||||
],
|
||||
declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
@@ -86,7 +94,11 @@ describe('JournalComponent', () => {
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: VersionHistoryDataService, useValue: {} },
|
||||
{ provide: VersionDataService, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: WorkspaceitemDataService, useValue: {} },
|
||||
{ provide: SearchService, useValue: {} },
|
||||
{ provide: RouteService, useValue: {} }
|
||||
],
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
|
||||
@listableObjectComponent('Journal', ViewMode.StandalonePage)
|
||||
@Component({
|
||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Journal
|
||||
*/
|
||||
export class JournalComponent extends ItemComponent {
|
||||
export class JournalComponent extends VersionedItemComponent {
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{{'orgunit.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['organization.legalName'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'orgunit.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,12 +57,12 @@
|
||||
[relationTypes]="[{
|
||||
label: 'isOrgUnitOfPerson',
|
||||
filter: 'isOrgUnitOfPerson',
|
||||
configuration: 'person'
|
||||
configuration: 'person-relationships'
|
||||
},
|
||||
{
|
||||
label: 'isOrgUnitOfProject',
|
||||
filter: 'isOrgUnitOfProject',
|
||||
configuration: 'project'
|
||||
configuration: 'project-relationships'
|
||||
}]">
|
||||
</ds-tabbed-related-entities-search>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
|
||||
@listableObjectComponent('OrgUnit', ViewMode.StandalonePage)
|
||||
@Component({
|
||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Organisation Unit
|
||||
*/
|
||||
export class OrgUnitComponent extends ItemComponent {
|
||||
export class OrgUnitComponent extends VersionedItemComponent {
|
||||
}
|
||||
|
@@ -3,6 +3,10 @@
|
||||
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object" class="mr-2"></ds-dso-page-orcid-button>
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-button>
|
||||
<ds-person-page-claim-button [object]="object"></ds-person-page-claim-button>
|
||||
</div>
|
||||
@@ -62,7 +66,8 @@
|
||||
<ds-tabbed-related-entities-search [item]="object"
|
||||
[relationTypes]="[{
|
||||
label: 'isAuthorOfPublication',
|
||||
filter: 'isAuthorOfPublication'
|
||||
filter: 'isAuthorOfPublication',
|
||||
configuration: 'default-relationships'
|
||||
}]">
|
||||
</ds-tabbed-related-entities-search>
|
||||
</div>
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import {
|
||||
listableObjectComponent
|
||||
} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||
|
||||
@listableObjectComponent('Person', ViewMode.StandalonePage)
|
||||
@@ -15,7 +13,7 @@ import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Person
|
||||
*/
|
||||
export class PersonComponent extends ItemComponent {
|
||||
export class PersonComponent extends VersionedItemComponent {
|
||||
|
||||
/**
|
||||
* Returns the metadata values to be used for the page title.
|
||||
@@ -36,4 +34,5 @@ export class PersonComponent extends ItemComponent {
|
||||
}
|
||||
return metadataValues;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@
|
||||
{{'project.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||
</h2>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-version-button (newVersionEvent)="onCreateNewVersion()" [dso]="object"
|
||||
[tooltipMsgCreate]="'item.page.version.create'"
|
||||
[tooltipMsgHasDraft]="'item.page.version.hasDraft'"></ds-dso-page-version-button>
|
||||
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'project.page.edit'"></ds-dso-page-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component';
|
||||
|
||||
@listableObjectComponent('Project', ViewMode.StandalonePage)
|
||||
@Component({
|
||||
@@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
|
||||
/**
|
||||
* The component for displaying metadata and relations of an item of the type Project
|
||||
*/
|
||||
export class ProjectComponent extends ItemComponent {
|
||||
export class ProjectComponent extends VersionedItemComponent {
|
||||
}
|
||||
|
@@ -67,11 +67,11 @@
|
||||
<a class="text-white" href="javascript:void(0);"
|
||||
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<li *ngIf="showPrivacyPolicy">
|
||||
<a class="text-white"
|
||||
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<li *ngIf="showEndUserAgreement">
|
||||
<a class="text-white"
|
||||
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
|
||||
</li>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Optional } from '@angular/core';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-footer',
|
||||
@@ -14,6 +15,8 @@ export class FooterComponent {
|
||||
* A boolean representing if to show or not the top footer container
|
||||
*/
|
||||
showTopFooter = false;
|
||||
showPrivacyPolicy = environment.info.enablePrivacyStatement;
|
||||
showEndUserAgreement = environment.info.enableEndUserAgreement;
|
||||
|
||||
constructor(@Optional() private cookies: KlaroService) {
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
||||
<ds-search-navbar></ds-search-navbar>
|
||||
<ds-lang-switch></ds-lang-switch>
|
||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||
<div class="pl-2">
|
||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||
|
@@ -16,7 +16,7 @@ export class HealthStatusComponent {
|
||||
@Input() status: HealthStatus;
|
||||
|
||||
/**
|
||||
* He
|
||||
* Health Status
|
||||
*/
|
||||
HealthStatus = HealthStatus;
|
||||
|
||||
|
@@ -10,4 +10,4 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="communitiesRD?.isLoading " message="{{'loading.top-level-communities' | translate}}"></ds-loading></ng-container>
|
||||
<ds-themed-loading *ngIf="communitiesRD?.isLoading " message="{{'loading.top-level-communities' | translate}}"></ds-themed-loading></ng-container>
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ImportExternalPageComponent } from './import-external-page.component';
|
||||
import { ThemeService } from '../shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../shared/mocks/theme-service.mock';
|
||||
|
||||
describe('ImportExternalPageComponent', () => {
|
||||
let component: ImportExternalPageComponent;
|
||||
@@ -9,6 +11,9 @@ describe('ImportExternalPageComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ImportExternalPageComponent ],
|
||||
providers:[
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -76,7 +76,7 @@ export class EndUserAgreementComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Cancel the agreement
|
||||
* If the user is logged in, this will log him/her out
|
||||
* If the user is logged in, this will log them out
|
||||
* If the user is not logged in, they will be redirected to the homepage
|
||||
*/
|
||||
cancel() {
|
||||
|
@@ -6,26 +6,10 @@ import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end
|
||||
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
||||
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
||||
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: END_USER_AGREEMENT_PATH,
|
||||
component: ThemedEndUserAgreementComponent,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
|
||||
}
|
||||
]),
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: PRIVACY_PATH,
|
||||
component: ThemedPrivacyComponent,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
|
||||
}
|
||||
]),
|
||||
const imports = [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: FEEDBACK_PATH,
|
||||
@@ -35,6 +19,34 @@ import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
||||
canActivate: [FeedbackGuard]
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
if (environment.info.enableEndUserAgreement) {
|
||||
imports.push(
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: END_USER_AGREEMENT_PATH,
|
||||
component: ThemedEndUserAgreementComponent,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
|
||||
}
|
||||
]));
|
||||
}
|
||||
if (environment.info.enablePrivacyStatement) {
|
||||
imports.push(
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: PRIVACY_PATH,
|
||||
component: ThemedPrivacyComponent,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
|
||||
}
|
||||
]));
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
...imports
|
||||
]
|
||||
})
|
||||
/**
|
||||
|
@@ -15,6 +15,7 @@ import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||
import { ThemedItemMetadataComponent } from './item-metadata/themed-item-metadata.component';
|
||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component';
|
||||
@@ -63,6 +64,7 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
|
||||
ItemDeleteComponent,
|
||||
ItemStatusComponent,
|
||||
ItemMetadataComponent,
|
||||
ThemedItemMetadataComponent,
|
||||
ItemRelationshipsComponent,
|
||||
ItemBitstreamsComponent,
|
||||
ItemVersionHistoryComponent,
|
||||
@@ -84,7 +86,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
|
||||
ObjectValuesPipe
|
||||
],
|
||||
exports: [
|
||||
ItemMetadataComponent
|
||||
EditInPlaceFieldComponent,
|
||||
ThemedItemMetadataComponent,
|
||||
]
|
||||
})
|
||||
export class EditItemPageModule {
|
||||
|
@@ -44,7 +44,7 @@
|
||||
class="alert alert-info w-100 d-inline-block mt-4" role="alert">
|
||||
{{'item.edit.bitstreams.empty' | translate}}
|
||||
</div>
|
||||
<ds-loading *ngIf="!bundles" message="{{'loading.bitstreams' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!bundles" message="{{'loading.bitstreams' | translate}}"></ds-themed-loading>
|
||||
|
||||
<div class="button-row bottom">
|
||||
<div class="mt-4 float-right space-children-mr ml-gap">
|
||||
|
@@ -50,3 +50,7 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .larger-tooltip .tooltip-inner {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
@@ -29,5 +29,5 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-loading>
|
||||
<ds-themed-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-themed-loading>
|
||||
</ds-pagination>
|
||||
|
@@ -9,9 +9,10 @@
|
||||
</div>
|
||||
<div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center">
|
||||
<div class="w-100">
|
||||
<span class="text-truncate">
|
||||
<div class="text-truncate" [tooltipClass]="'larger-tooltip'" placement="bottom"
|
||||
[ngbTooltip]="bitstream?.firstMetadataValue('dc.description')">
|
||||
{{ bitstream?.firstMetadataValue('dc.description') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center">
|
||||
|
@@ -0,0 +1,34 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { UpdateDataService } from '../../../core/data/update-data.service';
|
||||
import { ItemMetadataComponent } from './item-metadata.component';
|
||||
import { ThemedComponent } from '../../../shared/theme-support/themed.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-item-metadata',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
/**
|
||||
* Component for displaying an item's metadata edit page
|
||||
*/
|
||||
export class ThemedItemMetadataComponent extends ThemedComponent<ItemMetadataComponent> {
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
@Input() updateService: UpdateDataService<Item>;
|
||||
|
||||
protected inAndOutputNames: (keyof ItemMetadataComponent & keyof this)[] = ['item', 'updateService'];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'ItemMetadataComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../themes/${themeName}/app/item-page/edit-item-page/item-metadata/item-metadata.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./item-metadata.component`);
|
||||
}
|
||||
}
|
@@ -31,5 +31,5 @@
|
||||
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
|
||||
</ng-container>
|
||||
|
@@ -32,7 +32,7 @@
|
||||
></ds-edit-relationship-list>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="!relationshipTypes"></ds-loading>
|
||||
<ds-themed-loading *ngIf="!relationshipTypes"></ds-themed-loading>
|
||||
</ng-container>
|
||||
<div class="button-row bottom">
|
||||
<div class="float-right space-children-mr ml-gap">
|
||||
|
@@ -27,6 +27,8 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||
import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
|
||||
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock';
|
||||
import { ThemeService } from '../../../shared/theme-support/theme.service';
|
||||
import { getMockThemeService } from '../../../shared/mocks/theme-service.mock';
|
||||
|
||||
let comp: any;
|
||||
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
||||
@@ -211,6 +213,7 @@ describe('ItemRelationshipsComponent', () => {
|
||||
imports: [SharedModule, TranslateModule.forRoot()],
|
||||
declarations: [ItemRelationshipsComponent],
|
||||
providers: [
|
||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||
{ provide: ItemDataService, useValue: itemService },
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: Router, useValue: router },
|
||||
|
@@ -30,7 +30,7 @@ export class CollectionsComponent implements OnInit {
|
||||
|
||||
label = 'item.page.collections';
|
||||
|
||||
separator = '<br/>';
|
||||
@Input() separator = '<br/>';
|
||||
|
||||
/**
|
||||
* Amount of mapped collections that should be fetched at once.
|
||||
|
@@ -43,5 +43,5 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
|
||||
<ds-themed-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
|
@@ -50,3 +50,4 @@ export const ITEM_EDIT_PATH = 'edit';
|
||||
export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory';
|
||||
export const ITEM_VERSION_PATH = 'version';
|
||||
export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new';
|
||||
export const ORCID_PATH = 'orcid';
|
||||
|
@@ -7,15 +7,19 @@ import { VersionResolver } from './version-page/version.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
import { MenuItemType } from '../shared/menu/menu-item-type.model';
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||
import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
import {
|
||||
BitstreamRequestACopyPageComponent
|
||||
} from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||
import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -50,6 +54,11 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
{
|
||||
path: REQUEST_COPY_MODULE_PATH,
|
||||
component: BitstreamRequestACopyPageComponent,
|
||||
},
|
||||
{
|
||||
path: ORCID_PATH,
|
||||
component: OrcidPageComponent,
|
||||
canActivate: [AuthenticatedGuard, OrcidPageGuard]
|
||||
}
|
||||
],
|
||||
data: {
|
||||
@@ -88,6 +97,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||
LinkService,
|
||||
ItemPageAdministratorGuard,
|
||||
VersionResolver,
|
||||
OrcidPageGuard
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -6,11 +6,19 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { ItemPageComponent } from './simple/item-page.component';
|
||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
||||
import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component';
|
||||
import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||
import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||
import {
|
||||
ItemPageAuthorFieldComponent
|
||||
} from './simple/field-components/specific-field/author/item-page-author-field.component';
|
||||
import {
|
||||
ItemPageDateFieldComponent
|
||||
} from './simple/field-components/specific-field/date/item-page-date-field.component';
|
||||
import {
|
||||
ItemPageAbstractFieldComponent
|
||||
} from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
||||
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import {
|
||||
ItemPageTitleFieldComponent
|
||||
} from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
@@ -20,7 +28,9 @@ import { ItemComponent } from './simple/item-types/shared/item.component';
|
||||
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||
import {
|
||||
AbstractIncrementalListComponent
|
||||
} from './simple/abstract-incremental-list/abstract-incremental-list.component';
|
||||
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
@@ -34,6 +44,11 @@ import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.componen
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
|
||||
import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
|
||||
import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component';
|
||||
import { OrcidPageComponent } from './orcid-page/orcid-page.component';
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
|
||||
import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
|
||||
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -67,6 +82,10 @@ const DECLARATIONS = [
|
||||
MediaViewerImageComponent,
|
||||
MiradorViewerComponent,
|
||||
VersionPageComponent,
|
||||
OrcidPageComponent,
|
||||
OrcidAuthComponent,
|
||||
OrcidSyncSettingsComponent,
|
||||
OrcidQueueComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -79,6 +98,7 @@ const DECLARATIONS = [
|
||||
JournalEntitiesModule.withEntryComponents(),
|
||||
ResearchEntitiesModule.withEntryComponents(),
|
||||
NgxGalleryModule,
|
||||
NgbAccordionModule
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<ng-container *ngVar="mediaList$ | async as mediaList">
|
||||
<ds-loading
|
||||
<ds-themed-loading
|
||||
*ngIf="isLoading"
|
||||
message="{{ 'loading.default' | translate }}"
|
||||
[showMessage]="false"
|
||||
></ds-loading>
|
||||
></ds-themed-loading>
|
||||
<div class="media-viewer" *ngIf="!isLoading">
|
||||
<ng-container *ngIf="mediaList.length > 0">
|
||||
<ng-container *ngIf="videoOptions">
|
||||
|
@@ -110,7 +110,7 @@ describe('MediaViewerComponent', () => {
|
||||
});
|
||||
|
||||
it('should display a loading component', () => {
|
||||
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
||||
const loading = fixture.debugElement.query(By.css('ds-themed-loading'));
|
||||
expect(loading.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@@ -0,0 +1,84 @@
|
||||
<div class="container mb-5">
|
||||
<h2>{{'person.orcid.registry.auth' | translate}}</h2>
|
||||
<ng-container *ngIf="(isLinkedToOrcid() | async); then orcidLinked; else orcidNotLinked"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #orcidLinked>
|
||||
<div data-test="orcidLinked">
|
||||
<div class="row">
|
||||
<div *ngIf="(hasOrcidAuthorizations() | async)" class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container p-0">
|
||||
<ul>
|
||||
<li *ngFor="let auth of (getOrcidAuthorizations() | async)" data-test="orcidAuthorization">
|
||||
{{getAuthorizationDescription(auth) | translate}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<ds-alert *ngIf="!(hasMissingOrcidAuthorizations() | async)" [type]="'alert-success'" data-test="noMissingOrcidAuthorizations">
|
||||
{{'person.page.orcid.no-missing-authorizations-message' | translate}}
|
||||
</ds-alert>
|
||||
<ds-alert *ngIf="(hasMissingOrcidAuthorizations() | async)" [type]="'alert-warning'" data-test="missingOrcidAuthorizations">
|
||||
{{'person.page.orcid.missing-authorizations-message' | translate}}
|
||||
<ul>
|
||||
<li *ngFor="let auth of (getMissingOrcidAuthorizations() | async)" data-test="missingOrcidAuthorization">
|
||||
{{getAuthorizationDescription(auth) | translate }}
|
||||
</li>
|
||||
</ul>
|
||||
</ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-alert *ngIf="(onlyAdminCanDisconnectProfileFromOrcid() | async) && !(ownerCanDisconnectProfileFromOrcid() | async)"
|
||||
[type]="'alert-warning'" data-test="unlinkOnlyAdmin">
|
||||
{{ 'person.page.orcid.remove-orcid-message' | translate}}
|
||||
</ds-alert>
|
||||
<div class="row" *ngIf="(ownerCanDisconnectProfileFromOrcid() | async)" data-test="unlinkOwner">
|
||||
<div class="col">
|
||||
<button type="submit" class="btn btn-danger float-right" (click)="unlinkOrcid()"
|
||||
[disabled]="(unlinkProcessing | async)">
|
||||
<span *ngIf="!(unlinkProcessing | async)"><i
|
||||
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
|
||||
<span *ngIf="(unlinkProcessing | async)"><i
|
||||
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
|
||||
</button>
|
||||
<button *ngIf="(hasMissingOrcidAuthorizations() | async)" type="submit"
|
||||
class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #orcidNotLinked>
|
||||
<div data-test="orcidNotLinked">
|
||||
<div class="row">
|
||||
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
|
||||
<div class="col">
|
||||
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<button class="btn btn-primary float-right" (click)="linkOrcid()">
|
||||
<i class="fas fa-link"></i>
|
||||
{{'person.page.orcid.link' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -0,0 +1,336 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { of } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { OrcidAuthComponent } from './orcid-auth.component';
|
||||
import { NativeWindowService } from '../../../core/services/window.service';
|
||||
import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
|
||||
describe('OrcidAuthComponent test suite', () => {
|
||||
let comp: OrcidAuthComponent;
|
||||
let fixture: ComponentFixture<OrcidAuthComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||
let nativeWindowRef;
|
||||
let notificationsService;
|
||||
|
||||
const orcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited',
|
||||
'/activities/update',
|
||||
'/person/update'
|
||||
];
|
||||
|
||||
const partialOrcidScopes = [
|
||||
'/authenticate',
|
||||
'/read-limited',
|
||||
];
|
||||
|
||||
const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [{
|
||||
value: 'test person'
|
||||
}],
|
||||
'dspace.entity.type': [{
|
||||
'value': 'Person'
|
||||
}],
|
||||
'dspace.object.owner': [{
|
||||
'value': 'test person',
|
||||
'language': null,
|
||||
'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff',
|
||||
'confidence': 600,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.authenticated': [{
|
||||
'value': '2022-06-10T15:15:12.952872',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}],
|
||||
'dspace.orcid.scope': [{
|
||||
'value': '/authenticate',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}, {
|
||||
'value': '/read-limited',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 1
|
||||
}, {
|
||||
'value': '/activities/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 2
|
||||
}, {
|
||||
'value': '/person/update',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 3
|
||||
}],
|
||||
'person.identifier.orcid': [{
|
||||
'value': 'orcid-id',
|
||||
'language': null,
|
||||
'authority': null,
|
||||
'confidence': -1,
|
||||
'place': 0
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
orcidAuthService = jasmine.createSpyObj('researcherProfileService', {
|
||||
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
|
||||
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
|
||||
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
|
||||
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||
onlyAdminCanDisconnectProfileFromOrcid: jasmine.createSpy('onlyAdminCanDisconnectProfileFromOrcid'),
|
||||
ownerCanDisconnectProfileFromOrcid: jasmine.createSpy('ownerCanDisconnectProfileFromOrcid'),
|
||||
unlinkOrcidByItem: jasmine.createSpy('unlinkOrcidByItem')
|
||||
});
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbAccordionModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [OrcidAuthComponent],
|
||||
providers: [
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||
{ provide: OrcidAuthService, useValue: orcidAuthService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(OrcidAuthComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
fixture = TestBed.createComponent(OrcidAuthComponent);
|
||||
comp = fixture.componentInstance;
|
||||
orcidAuthService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
|
||||
}));
|
||||
|
||||
describe('when orcid profile is not linked', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemUnlinkedToOrcid;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(false);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
orcidAuthService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeFalsy();
|
||||
expect(orcidNotLinked).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should change location on link', () => {
|
||||
nativeWindowRef = (comp as any)._window;
|
||||
scheduler.schedule(() => comp.linkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(nativeWindowRef.nativeWindow.location.href).toBe('oarcidUrl');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when orcid profile is linked', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
notificationsService = (comp as any).notificationsService;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
}));
|
||||
|
||||
describe('and unlink is successfully', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
|
||||
spyOn(comp.unlink, 'emit');
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should show success notification', () => {
|
||||
scheduler.schedule(() => comp.unlinkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.success).toHaveBeenCalled();
|
||||
expect(comp.unlink.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and unlink is failed', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should show success notification', () => {
|
||||
scheduler.schedule(() => comp.unlinkOrcid());
|
||||
scheduler.flush();
|
||||
|
||||
expect(notificationsService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and has orcid authorization scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeTruthy();
|
||||
expect(orcidNotLinked).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should display orcid authorizations', fakeAsync(() => {
|
||||
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||
const noMissingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="noMissingOrcidAuthorizations"]'));
|
||||
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||
|
||||
expect(orcidAuthorizations).toBeTruthy();
|
||||
expect(noMissingOrcidAuthorizations).toBeTruthy();
|
||||
expect(orcidAuthorizationsList.length).toBe(4);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and has missing orcid authorization scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', fakeAsync(() => {
|
||||
const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]'));
|
||||
const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]'));
|
||||
expect(orcidLinked).toBeTruthy();
|
||||
expect(orcidNotLinked).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should display orcid authorizations', fakeAsync(() => {
|
||||
const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]'));
|
||||
const missingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="missingOrcidAuthorizations"]'));
|
||||
const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]'));
|
||||
const missingOrcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="missingOrcidAuthorization"]'));
|
||||
|
||||
expect(orcidAuthorizations).toBeTruthy();
|
||||
expect(missingOrcidAuthorizations).toBeTruthy();
|
||||
expect(orcidAuthorizationsList.length).toBe(2);
|
||||
expect(missingOrcidAuthorizationsList.length).toBe(2);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and only admin can unlink scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should display warning panel', fakeAsync(() => {
|
||||
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||
expect(unlinkOnlyAdmin).toBeTruthy();
|
||||
expect(unlinkOwner).toBeFalsy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('and owner can unlink scopes', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
comp.item = mockItemLinkedToOrcid;
|
||||
orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
|
||||
orcidAuthService.isLinkedToOrcid.and.returnValue(true);
|
||||
orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should display warning panel', fakeAsync(() => {
|
||||
const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]'));
|
||||
const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]'));
|
||||
expect(unlinkOnlyAdmin).toBeFalsy();
|
||||
expect(unlinkOwner).toBeTruthy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
218
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
218
src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-orcid-auth',
|
||||
templateUrl: './orcid-auth.component.html',
|
||||
styleUrls: ['./orcid-auth.component.scss']
|
||||
})
|
||||
export class OrcidAuthComponent implements OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* The item for which showing the orcid settings
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
/**
|
||||
* The list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
profileAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* The list of all orcid authorization scopes missing in the orcid profile
|
||||
*/
|
||||
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* The list of all orcid authorization scopes available
|
||||
*/
|
||||
orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
|
||||
|
||||
/**
|
||||
* A boolean representing if unlink operation is processing
|
||||
*/
|
||||
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if orcid profile is linked
|
||||
*/
|
||||
private isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if only admin can disconnect orcid profile
|
||||
*/
|
||||
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* A boolean representing if owner can disconnect orcid profile
|
||||
*/
|
||||
private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* An event emitted when orcid profile is unliked successfully
|
||||
*/
|
||||
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private orcidAuthService: OrcidAuthService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.orcidAuthService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
|
||||
this.orcidAuthorizationScopes.next(scopes);
|
||||
this.initOrcidAuthSettings();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
|
||||
this.initOrcidAuthSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||
*/
|
||||
hasOrcidAuthorizations(): Observable<boolean> {
|
||||
return this.profileAuthorizationScopes.asObservable().pipe(
|
||||
map((scopes: string[]) => scopes.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
getOrcidAuthorizations(): Observable<string[]> {
|
||||
return this.profileAuthorizationScopes.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list of exposed orcid authorization scopes for the orcid profile has values
|
||||
*/
|
||||
hasMissingOrcidAuthorizations(): Observable<boolean> {
|
||||
return this.missingAuthorizationScopes.asObservable().pipe(
|
||||
map((scopes: string[]) => scopes.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of exposed orcid authorization scopes for the orcid profile
|
||||
*/
|
||||
getMissingOrcidAuthorizations(): Observable<string[]> {
|
||||
return this.profileAuthorizationScopes.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if orcid profile is linked
|
||||
*/
|
||||
isLinkedToOrcid(): Observable<boolean> {
|
||||
return this.isOrcidLinked$.asObservable();
|
||||
}
|
||||
|
||||
getOrcidNotLinkedMessage(): Observable<string> {
|
||||
const orcid = this.item.firstMetadataValue('person.identifier.orcid');
|
||||
if (orcid) {
|
||||
return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid });
|
||||
} else {
|
||||
return this.translateService.get('person.page.orcid.no-orcid-message');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get label for a given orcid authorization scope
|
||||
*
|
||||
* @param scope
|
||||
*/
|
||||
getAuthorizationDescription(scope: string) {
|
||||
return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if only admin can disconnect orcid profile
|
||||
*/
|
||||
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean representing if owner can disconnect orcid profile
|
||||
*/
|
||||
ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||
return this.ownerCanDisconnectProfileFromOrcid$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Link existing person profile with orcid
|
||||
*/
|
||||
linkOrcid(): void {
|
||||
this.orcidAuthService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
|
||||
this._window.nativeWindow.location.href = authorizeUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink existing person profile from orcid
|
||||
*/
|
||||
unlinkOrcid(): void {
|
||||
this.unlinkProcessing.next(true);
|
||||
this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
|
||||
this.unlinkProcessing.next(false);
|
||||
if (remoteData.isSuccess) {
|
||||
this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success'));
|
||||
this.unlink.emit();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize all Orcid authentication settings
|
||||
* @private
|
||||
*/
|
||||
private initOrcidAuthSettings(): void {
|
||||
|
||||
this.setOrcidAuthorizationsFromItem();
|
||||
|
||||
this.setMissingOrcidAuthorizations();
|
||||
|
||||
this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
|
||||
});
|
||||
|
||||
this.orcidAuthService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
|
||||
this.ownerCanDisconnectProfileFromOrcid$.next(result);
|
||||
});
|
||||
|
||||
this.isOrcidLinked$.next(this.orcidAuthService.isLinkedToOrcid(this.item));
|
||||
}
|
||||
|
||||
private setMissingOrcidAuthorizations(): void {
|
||||
const profileScopes = this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item);
|
||||
const orcidScopes = this.orcidAuthorizationScopes.value;
|
||||
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
|
||||
|
||||
this.missingAuthorizationScopes.next(missingScopes);
|
||||
}
|
||||
|
||||
private setOrcidAuthorizationsFromItem(): void {
|
||||
this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
|
||||
}
|
||||
|
||||
}
|
19
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
19
src/app/item-page/orcid-page/orcid-page.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div *ngIf="!(processingConnection | async) && (item | async)" class="container">
|
||||
<div class="button-row bottom mb-3">
|
||||
<div class="text-right">
|
||||
<a [routerLink]="getItemPage()" role="button" class="btn btn-outline-secondary" data-test="back-button">
|
||||
<i class="fas fa-arrow-left"></i> {{'item.orcid.return' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-loading *ngIf="(processingConnection | async)" [message]="'person.page.orcid.link.processing' | translate"></ds-loading>
|
||||
<div class="container" *ngIf="!(processingConnection | async) && !(connectionStatus | async)" data-test="error-box">
|
||||
<ds-alert [type]="'alert-danger'">{{'person.page.orcid.link.error.message' | translate}}</ds-alert>
|
||||
</div>
|
||||
<ng-container *ngIf="!(processingConnection | async) && (item | async) && (connectionStatus | async)" >
|
||||
<ds-orcid-auth [item]="(item | async)" (unlink)="updateItem()" data-test="orcid-auth"></ds-orcid-auth>
|
||||
<ds-orcid-sync-setting *ngIf="isLinkedToOrcid()" [item]="(item | async)" (settingsUpdated)="updateItem()" data-test="orcid-sync-setting"></ds-orcid-sync-setting>
|
||||
<ds-orcid-queue *ngIf="isLinkedToOrcid()" [item]="(item | async)"></ds-orcid-queue>
|
||||
</ng-container>
|
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
220
src/app/item-page/orcid-page/orcid-page.component.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||
import { OrcidPageComponent } from './orcid-page.component';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
|
||||
|
||||
describe('OrcidPageComponent test suite', () => {
|
||||
let comp: OrcidPageComponent;
|
||||
let fixture: ComponentFixture<OrcidPageComponent>;
|
||||
let scheduler: TestScheduler;
|
||||
let authService: jasmine.SpyObj<AuthService>;
|
||||
let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
|
||||
let routeData: any;
|
||||
let itemDataService: jasmine.SpyObj<ItemDataService>;
|
||||
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
|
||||
|
||||
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
|
||||
id: 'test-id',
|
||||
visible: true,
|
||||
type: 'profile',
|
||||
_links: {
|
||||
item: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-id/item'
|
||||
},
|
||||
self: {
|
||||
href: 'https://rest.api/rest/api/profiles/test-id'
|
||||
},
|
||||
}
|
||||
});
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
id: 'test-id',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'test item'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
|
||||
id: 'test-id',
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'test item'
|
||||
}
|
||||
],
|
||||
'dspace.orcid.authenticated': [
|
||||
{
|
||||
value: 'true'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: jasmine.createSpy('isAuthenticated'),
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
|
||||
routeData = {
|
||||
dso: createSuccessfulRemoteDataObject(mockItem),
|
||||
};
|
||||
|
||||
routeStub = new ActivatedRouteStub({}, routeData);
|
||||
|
||||
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
|
||||
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
|
||||
});
|
||||
|
||||
itemDataService = jasmine.createSpyObj('ItemDataService', {
|
||||
findById: jasmine.createSpy('findById')
|
||||
});
|
||||
|
||||
void TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [OrcidPageComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: ItemDataService, useValue: itemDataService },
|
||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
scheduler = getTestScheduler();
|
||||
fixture = TestBed.createComponent(OrcidPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
authService.isAuthenticated.and.returnValue(observableOf(true));
|
||||
}));
|
||||
|
||||
describe('whn has no query param', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(auth).toBeTruthy();
|
||||
expect(settings).toBeTruthy();
|
||||
expect(comp.itemId).toBe('test-id');
|
||||
});
|
||||
|
||||
it('should call isLinkedToOrcid', () => {
|
||||
comp.isLinkedToOrcid();
|
||||
|
||||
expect(orcidAuthService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
|
||||
});
|
||||
|
||||
it('should update item', fakeAsync(() => {
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
scheduler.schedule(() => comp.updateItem());
|
||||
scheduler.flush();
|
||||
|
||||
expect(comp.item.value).toEqual(mockItemLinkedToOrcid);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when query param contains orcid code', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
spyOn(comp, 'updateItem').and.callThrough();
|
||||
routeStub.testParams = {
|
||||
code: 'orcid-code'
|
||||
};
|
||||
}));
|
||||
|
||||
describe('and linking to orcid profile is successfully', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
orcidAuthService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should call linkOrcidByItem', () => {
|
||||
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||
expect(comp.updateItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(auth).toBeTruthy();
|
||||
expect(settings).toBeTruthy();
|
||||
expect(comp.itemId).toBe('test-id');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and linking to orcid profile is failed', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
orcidAuthService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
|
||||
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should call linkOrcidByItem', () => {
|
||||
expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
|
||||
expect(comp.updateItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
|
||||
const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
|
||||
const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
|
||||
const error = fixture.debugElement.query(By.css('[data-test="error-box"]'));
|
||||
expect(comp).toBeTruthy();
|
||||
expect(btn.length).toBe(1);
|
||||
expect(error).toBeTruthy();
|
||||
expect(auth).toBeFalsy();
|
||||
expect(settings).toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user