Merge remote-tracking branch 'origin/main' into CST-6110

This commit is contained in:
Nikunj Sharma
2022-08-16 17:53:08 +05:30
206 changed files with 2862 additions and 774 deletions

View File

@@ -22,7 +22,7 @@ jobs:
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
node-version: [12.x, 14.x] node-version: [14.x, 16.x]
# Do NOT exit immediately if one matrix job fails # Do NOT exit immediately if one matrix job fails
fail-fast: false fail-fast: false
# These are the actual CI steps to perform per job # These are the actual CI steps to perform per job
@@ -82,11 +82,11 @@ jobs:
run: yarn run test:headless run: yarn run test:headless
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 # 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 # https://github.com/codecov/codecov-action
- name: Upload coverage to Codecov.io - name: Upload coverage to Codecov.io
uses: codecov/codecov-action@v2 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 # Using docker-compose start backend using CI configuration
# and load assetstore from a cached copy # and load assetstore from a cached copy

View File

@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start 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 ```bash
# clone the repo # clone the repo
@@ -90,7 +90,7 @@ Requirements
------------ ------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) - [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
- Ensure you're running node `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. 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.

View File

@@ -63,7 +63,8 @@
"bundleName": "dspace-theme" "bundleName": "dspace-theme"
} }
], ],
"scripts": [] "scripts": [],
"baseHref": "/"
}, },
"configurations": { "configurations": {
"development": { "development": {

View File

@@ -2,7 +2,8 @@
debug: false debug: false
# Angular Universal server settings # 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: ui:
ssl: false ssl: false
host: localhost host: localhost
@@ -15,7 +16,8 @@ ui:
max: 500 # limit each IP to 500 requests per windowMs max: 500 # limit each IP to 500 requests per windowMs
# The REST API server settings # 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: rest:
ssl: true ssl: true
host: api7.dspace.org host: api7.dspace.org
@@ -246,3 +248,10 @@ bundle:
mediaViewer: mediaViewer:
image: false image: false
video: 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

View File

@@ -1,7 +1,9 @@
# Docker Compose files # Docker Compose files
*** ***
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario. :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 ## 'Dockerfile' in root directory

View File

@@ -107,7 +107,7 @@
"mirador": "^3.3.0", "mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0", "mirador-share-plugin": "^0.11.0",
"moment": "^2.29.2", "moment": "^2.29.4",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "^13.1.1", "ng-mocks": "^13.1.1",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "1.4.0",

View File

@@ -45,7 +45,7 @@
</div> </div>
</form> </form>
<ds-loading *ngIf="searching$ | async"></ds-loading> <ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)" *ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
[paginationOptions]="config" [paginationOptions]="config"

View File

@@ -36,12 +36,12 @@
</button> </button>
</ds-form> </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"> <div *ngIf="epersonService.getActiveEPerson() | async">
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5> <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 <ds-pagination
*ngIf="(groups | async)?.payload?.totalElements > 0" *ngIf="(groups | async)?.payload?.totalElements > 0"

View File

@@ -33,7 +33,7 @@
</div> </div>
</form> </form>
<ds-loading *ngIf="loading$ | async"></ds-loading> <ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
<ds-pagination <ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)" *ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
[paginationOptions]="config" [paginationOptions]="config"

View File

@@ -20,6 +20,8 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import createSpy = jasmine.createSpy; import createSpy = jasmine.createSpy;
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { Item } from '../../core/shared/item.model'; 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', () => { describe('AdminSidebarComponent', () => {
let comp: AdminSidebarComponent; let comp: AdminSidebarComponent;
@@ -60,6 +62,7 @@ describe('AdminSidebarComponent', () => {
declarations: [AdminSidebarComponent], declarations: [AdminSidebarComponent],
providers: [ providers: [
Injector, Injector,
{ provide: ThemeService, useValue: getMockThemeService() },
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: AuthService, useClass: AuthServiceStub }, { provide: AuthService, useClass: AuthServiceStub },

View File

@@ -9,6 +9,7 @@ import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../../shared/menu/menu-id.model'; import { MenuID } from '../../shared/menu/menu-id.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ThemeService } from '../../shared/theme-support/theme.service';
/** /**
* Component representing the admin sidebar * Component representing the admin sidebar
@@ -56,9 +57,10 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
private variableService: CSSVariableService, private variableService: CSSVariableService,
private authService: AuthService, private authService: AuthService,
public authorizationService: AuthorizationDataService, 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); this.inFocus$ = new BehaviorSubject(false);
} }

View File

@@ -27,7 +27,7 @@
</div> </div>
</div> </div>
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error> <ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
<ds-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading" <ds-themed-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
message="{{'loading.bitstream' | translate}}"></ds-loading> message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
</div> </div>
</ng-container> </ng-container>

View File

@@ -40,7 +40,7 @@
(prev)="goPrev()" (prev)="goPrev()"
(next)="goNext()"> (next)="goNext()">
</ds-browse-by> </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> </div>
</section> </section>
<ng-container *ngVar="(parent$ | async) as parent"> <ng-container *ngVar="(parent$ | async) as parent">

View File

@@ -1,6 +1,10 @@
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } from '../../shared/empty.util';
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import {
DEFAULT_THEME,
resolveTheme
} from '../../shared/object-collection/shared/listable-object/listable-object.decorator';
export enum BrowseByDataType { export enum BrowseByDataType {
Title = 'title', Title = 'title',
@@ -10,7 +14,7 @@ export enum BrowseByDataType {
export const DEFAULT_BROWSE_BY_TYPE = BrowseByDataType.Metadata; 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', providedIn: 'root',
factory: () => getComponentByBrowseByType factory: () => getComponentByBrowseByType
}); });
@@ -20,13 +24,17 @@ const map = new Map();
/** /**
* Decorator used for rendering Browse-By pages by type * Decorator used for rendering Browse-By pages by type
* @param browseByType The type of page * @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) { return function decorator(component: any) {
if (hasNoValue(map.get(browseByType))) { 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 { } 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 * Get the component used for rendering a Browse-By page by type
* @param browseByType The type of page * @param browseByType The type of page
* @param theme the theme to match
*/ */
export function getComponentByBrowseByType(browseByType) { export function getComponentByBrowseByType(browseByType, theme) {
const comp = map.get(browseByType); let themeMap = map.get(browseByType);
if (hasNoValue(themeMap)) {
themeMap = map.get(DEFAULT_BROWSE_BY_TYPE);
}
const comp = resolveTheme(themeMap, theme);
if (hasNoValue(comp)) { if (hasNoValue(comp)) {
map.get(DEFAULT_BROWSE_BY_TYPE); return themeMap.get(DEFAULT_THEME);
} }
return comp; return comp;
} }

View File

@@ -4,7 +4,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator'; import { BROWSE_BY_COMPONENT_FACTORY, BrowseByDataType } from './browse-by-decorator';
import { BrowseDefinition } from '../../core/shared/browse-definition.model'; 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', () => { describe('BrowseBySwitcherComponent', () => {
let comp: BrowseBySwitcherComponent; let comp: BrowseBySwitcherComponent;
@@ -44,11 +45,20 @@ describe('BrowseBySwitcherComponent', () => {
data data
}; };
let themeService: ThemeService;
let themeName: string;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
themeName = 'dspace';
themeService = jasmine.createSpyObj('themeService', {
getThemeName: themeName,
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [BrowseBySwitcherComponent], declarations: [BrowseBySwitcherComponent],
providers: [ providers: [
{ provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: ThemeService, useValue: themeService },
{ provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) } { provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -68,7 +78,7 @@ describe('BrowseBySwitcherComponent', () => {
}); });
it(`should call getComponentByBrowseByType with type "${type.dataType}"`, () => { 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);
}); });
}); });
}); });

View File

@@ -5,6 +5,7 @@ import { map } from 'rxjs/operators';
import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator';
import { GenericConstructor } from '../../core/shared/generic-constructor'; import { GenericConstructor } from '../../core/shared/generic-constructor';
import { BrowseDefinition } from '../../core/shared/browse-definition.model'; import { BrowseDefinition } from '../../core/shared/browse-definition.model';
import { ThemeService } from '../../shared/theme-support/theme.service';
@Component({ @Component({
selector: 'ds-browse-by-switcher', selector: 'ds-browse-by-switcher',
@@ -21,7 +22,8 @@ export class BrowseBySwitcherComponent implements OnInit {
browseByComponent: Observable<any>; browseByComponent: Observable<any>;
public constructor(protected route: ActivatedRoute, 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 { ngOnInit(): void {
this.browseByComponent = this.route.data.pipe( 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()))
); );
} }

View File

@@ -6,7 +6,7 @@ import { CreateCollectionPageComponent } from './create-collection-page/create-c
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-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 { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver'; import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver';
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
@@ -52,7 +52,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
}, },
{ {
path: ITEMTEMPLATE_PATH, path: ITEMTEMPLATE_PATH,
component: EditItemTemplatePageComponent, component: ThemedEditItemTemplatePageComponent,
canActivate: [AuthenticatedGuard], canActivate: [AuthenticatedGuard],
resolve: { resolve: {
item: ItemTemplatePageResolver, item: ItemTemplatePageResolver,

View File

@@ -56,8 +56,8 @@
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" <ds-error *ngIf="itemRD?.hasFailed"
message="{{'error.recent-submissions' | translate}}"></ds-error> message="{{'error.recent-submissions' | translate}}"></ds-error>
<ds-loading *ngIf="!itemRD || itemRD.isLoading" <ds-themed-loading *ngIf="!itemRD || itemRD.isLoading"
message="{{'loading.recent-submissions' | translate}}"></ds-loading> 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"> <div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
{{'collection.page.browse.recent.empty' | translate}} {{'collection.page.browse.recent.empty' | translate}}
</div> </div>
@@ -74,7 +74,7 @@
</div> </div>
<ds-error *ngIf="collectionRD?.hasFailed" <ds-error *ngIf="collectionRD?.hasFailed"
message="{{'error.collection' | translate}}"></ds-error> message="{{'error.collection' | translate}}"></ds-error>
<ds-loading *ngIf="collectionRD?.isLoading" <ds-themed-loading *ngIf="collectionRD?.isLoading"
message="{{'loading.collection' | translate}}"></ds-loading> message="{{'loading.collection' | translate}}"></ds-themed-loading>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-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 { 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 { EditItemPageModule } from '../item-page/edit-item-page/edit-item-page.module';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
@@ -32,6 +33,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
CreateCollectionPageComponent, CreateCollectionPageComponent,
DeleteCollectionPageComponent, DeleteCollectionPageComponent,
EditItemTemplatePageComponent, EditItemTemplatePageComponent,
ThemedEditItemTemplatePageComponent,
CollectionItemMapperComponent CollectionItemMapperComponent
], ],
providers: [ providers: [

View File

@@ -25,7 +25,7 @@
<label class="form-check-label" <label class="form-check-label"
for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label> for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
</div> </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> <h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
</div> </div>
<div class="row"> <div class="row">

View File

@@ -3,10 +3,10 @@
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD"> <div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
<ng-container *ngIf="itemRD?.hasSucceeded"> <ng-container *ngIf="itemRD?.hasSucceeded">
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2> <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> <button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
</ng-container> </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> <ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
</div> </div>
</div> </div>

View File

@@ -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');
}
}

View File

@@ -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"> <cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<!-- This is the tree node template for show more node --> <!-- This is the tree node template for show more node -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding <cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
@@ -12,7 +12,7 @@
class="btn btn-outline-primary btn-sm" role="button"> class="btn btn-outline-primary btn-sm" role="button">
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }} <i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
</a> </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> </div>
<div class="text-muted" cdkTreeNodePadding> <div class="text-muted" cdkTreeNodePadding>
@@ -57,7 +57,7 @@
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}" <span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span> aria-hidden="true"></span>
</button> </button>
<ds-loading class="ds-loading"></ds-loading> <ds-themed-loading class="ds-themed-loading"></ds-themed-loading>
</div> </div>
</cdk-tree-node> </cdk-tree-node>
<!-- This is the tree node template for leaf nodes (collections and (sub)coms without children) --> <!-- This is the tree node template for leaf nodes (collections and (sub)coms without children) -->

View File

@@ -41,5 +41,5 @@
</div> </div>
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error> <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> </div>

View File

@@ -9,5 +9,5 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error> <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> </ng-container>

View File

@@ -9,5 +9,5 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error> <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> </ng-container>

View File

@@ -173,6 +173,11 @@ import { LinkHeadService } from './services/link-head.service';
import { ResearcherProfileService } from './profile/researcher-profile.service'; import { ResearcherProfileService } from './profile/researcher-profile.service';
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
import { ResearcherProfile } from './profile/model/researcher-profile.model'; 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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -300,7 +305,10 @@ const PROVIDERS = [
GroupDataService, GroupDataService,
FeedbackDataService, FeedbackDataService,
ResearcherProfileService, ResearcherProfileService,
ProfileClaimService ProfileClaimService,
OrcidAuthService,
OrcidQueueService,
OrcidHistoryDataService,
]; ];
/** /**
@@ -362,7 +370,10 @@ export const models =
SearchConfig, SearchConfig,
SubmissionAccessesModel, SubmissionAccessesModel,
AccessStatusObject, AccessStatusObject,
ResearcherProfile ResearcherProfile,
OrcidQueue,
OrcidHistory,
AccessStatusObject
]; ];
@NgModule({ @NgModule({

View File

@@ -894,6 +894,26 @@ describe('DataService', () => {
expectObservable(done$).toBe('------(t|)', BOOLEAN); 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', () => { describe('delete', () => {

View File

@@ -595,6 +595,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
const done$ = new AsyncSubject<boolean>(); const done$ = new AsyncSubject<boolean>();
this.objectCache.getByHref(href).pipe( this.objectCache.getByHref(href).pipe(
take(1),
switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe( switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe(
mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)),
toArray(), toArray(),

View File

@@ -1,6 +1,7 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; 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 { 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 * 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 * when they're finished accepting the agreement
*/ */
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
if (!environment.info.enableEndUserAgreement) {
return observableOf(true);
}
return this.hasAccepted().pipe( return this.hasAccepted().pipe(
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url) returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
); );

View File

@@ -2,6 +2,7 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-u
import { EndUserAgreementService } from './end-user-agreement.service'; import { EndUserAgreementService } from './end-user-agreement.service';
import { Router, UrlTree } from '@angular/router'; import { Router, UrlTree } from '@angular/router';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { environment } from '../../../environments/environment.test';
describe('EndUserAgreementGuard', () => { describe('EndUserAgreementGuard', () => {
let guard: EndUserAgreementCurrentUserGuard; 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();
});
});
});
}); });
}); });

View File

@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core'; 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 { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
import { EndUserAgreementService } from './end-user-agreement.service'; import { EndUserAgreementService } from './end-user-agreement.service';
import { Router } from '@angular/router'; 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 * 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 * True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
*/ */
hasAccepted(): Observable<boolean> { hasAccepted(): Observable<boolean> {
if (!environment.info.enableEndUserAgreement) {
return observableOf(true);
}
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true); return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
} }

View 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,
};
}

View 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');

View 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,
};
}

View 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');

View 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);
});
});
});

View 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()
);
}
}

View 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);
}
}

View 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');
}
}

View File

@@ -12,7 +12,7 @@ describe('PaginationService', () => {
let routeService; let routeService;
const defaultPagination = new PaginationComponentOptions(); const defaultPagination = new PaginationComponentOptions();
const defaultSort = new SortOptions('id', SortDirection.DESC); const defaultSort = new SortOptions('dc.title', SortDirection.ASC);
const defaultFindListOptions = new FindListOptions(); const defaultFindListOptions = new FindListOptions();
beforeEach(() => { beforeEach(() => {
@@ -39,7 +39,6 @@ describe('PaginationService', () => {
service = new PaginationService(routeService, router); service = new PaginationService(routeService, router);
}); });
describe('getCurrentPagination', () => { describe('getCurrentPagination', () => {
it('should retrieve the current pagination info from the routerService', () => { it('should retrieve the current pagination info from the routerService', () => {
service.getCurrentPagination('test-id', defaultPagination).subscribe((currentPagination) => { service.getCurrentPagination('test-id', defaultPagination).subscribe((currentPagination) => {
@@ -56,6 +55,26 @@ describe('PaginationService', () => {
expect(currentSort).toEqual(Object.assign(new SortOptions('score', SortDirection.ASC ))); 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', () => { describe('getFindListOptions', () => {
it('should retrieve the current findListOptions info from the routerService', () => { it('should retrieve the current findListOptions info from the routerService', () => {

View File

@@ -24,7 +24,11 @@ import { isNumeric } from '../../shared/numeric.util';
*/ */
export class PaginationService { 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 = {}; private clearParams = {};

View File

@@ -12,7 +12,6 @@ import { RequestService } from '../data/request.service';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { buildPaginatedList } from '../data/paginated-list.model'; import { buildPaginatedList } from '../data/paginated-list.model';
import { import {
createFailedRemoteDataObject$,
createNoContentRemoteDataObject$, createNoContentRemoteDataObject$,
createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
@@ -23,15 +22,12 @@ import { ResearcherProfileService } from './researcher-profile.service';
import { RouterMock } from '../../shared/mocks/router.mock'; import { RouterMock } from '../../shared/mocks/router.mock';
import { ResearcherProfile } from './model/researcher-profile.model'; import { ResearcherProfile } from './model/researcher-profile.model';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { AddOperation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'; import { ReplaceOperation } from 'fast-json-patch';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from '../data/request.models'; import { PostRequest } from '../data/request.models';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { ConfigurationProperty } from '../shared/configuration-property.model'; import { ConfigurationProperty } from '../shared/configuration-property.model';
import { ConfigurationDataService } from '../data/configuration-data.service';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { NativeWindowRefMock } from '../../shared/mocks/mock-native-window-ref';
import { URLCombiner } from '../url-combiner/url-combiner';
describe('ResearcherProfileService', () => { describe('ResearcherProfileService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -42,8 +38,6 @@ describe('ResearcherProfileService', () => {
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
let halService: HALEndpointService; let halService: HALEndpointService;
let responseCacheEntry: RequestEntry; let responseCacheEntry: RequestEntry;
let configurationDataService: ConfigurationDataService;
let nativeWindowService: NativeWindowRefMock;
let routerStub: any; let routerStub: any;
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
@@ -252,13 +246,8 @@ describe('ResearcherProfileService', () => {
const itemService = jasmine.createSpyObj('ItemService', { const itemService = jasmine.createSpyObj('ItemService', {
findByHref: jasmine.createSpy('findByHref') findByHref: jasmine.createSpy('findByHref')
}); });
configurationDataService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: jasmine.createSpy('findByPropertyName')
});
nativeWindowService = new NativeWindowRefMock();
service = new ResearcherProfileService( service = new ResearcherProfileService(
nativeWindowService,
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache,
@@ -267,8 +256,7 @@ describe('ResearcherProfileService', () => {
http, http,
routerStub, routerStub,
comparator, comparator,
itemService, itemService
configurationDataService
); );
serviceAsAny = service; serviceAsAny = service;
@@ -415,121 +403,6 @@ describe('ResearcherProfileService', () => {
}); });
}); });
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();
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
});
it('should call patch method properly', () => {
const operations: AddOperation<string>[] = [{
path: '/orcid',
op: 'add',
value: 'test-code'
}];
scheduler.schedule(() => service.linkOrcidByItem(mockItemUnlinkedToOrcid, 'test-code').subscribe());
scheduler.flush();
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, operations);
});
});
describe('unlinkOrcidByItem', () => {
beforeEach(() => {
scheduler = getTestScheduler();
spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile));
});
it('should call patch method properly', () => {
const operations: RemoveOperation[] = [{
path: '/orcid',
op: 'remove'
}];
scheduler.schedule(() => service.unlinkOrcidByItem(mockItemLinkedToOrcid).subscribe());
scheduler.flush();
expect((service as any).dataService.patch).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('updateByOrcidOperations', () => { describe('updateByOrcidOperations', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
@@ -543,34 +416,4 @@ describe('ResearcherProfileService', () => {
expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []); expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []);
}); });
}); });
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);
});
});
}); });

View File

@@ -1,41 +1,33 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AddOperation, Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'; import { Operation, ReplaceOperation } from 'fast-json-patch';
import { combineLatest, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { find, map, switchMap } from 'rxjs/operators'; import { find, map } from 'rxjs/operators';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ConfigurationDataService } from '../data/configuration-data.service';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { ItemDataService } from '../data/item-data.service'; import { ItemDataService } from '../data/item-data.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ConfigurationProperty } from '../shared/configuration-property.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model'; import { NoContent } from '../shared/NoContent.model';
import { import { getAllCompletedRemoteData, getFirstCompletedRemoteData } from '../shared/operators';
getAllCompletedRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload
} from '../shared/operators';
import { ResearcherProfile } from './model/researcher-profile.model'; import { ResearcherProfile } from './model/researcher-profile.model';
import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type'; import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from '../data/request.models'; import { PostRequest } from '../data/request.models';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty } from '../../shared/empty.util';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { URLCombiner } from '../url-combiner/url-combiner';
/** /**
* A private DataService implementation to delegate specific methods to. * A private DataService implementation to delegate specific methods to.
@@ -69,7 +61,6 @@ export class ResearcherProfileService {
protected responseMsToLive: number = 10 * 1000; protected responseMsToLive: number = 10 * 1000;
constructor( constructor(
@Inject(NativeWindowService) protected _window: NativeWindowRef,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
@@ -78,8 +69,7 @@ export class ResearcherProfileService {
protected http: HttpClient, protected http: HttpClient,
protected router: Router, protected router: Router,
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>, protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
protected itemService: ItemDataService, protected itemService: ItemDataService) {
protected configurationService: ConfigurationDataService) {
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService, this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService,
notificationsService, http, comparator); notificationsService, http, comparator);
@@ -165,98 +155,6 @@ export class ResearcherProfileService {
return this.dataService.patch(researcherProfile, [replaceOperation]); return this.dataService.patch(researcherProfile, [replaceOperation]);
} }
/**
* 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.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
getFirstCompletedRemoteData(),
switchMap((profileRD) => this.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.findById(person.firstMetadata('dspace.object.owner').authority).pipe(
getFirstCompletedRemoteData(),
switchMap((profileRD) => this.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]) => {
console.log(this._window.nativeWindow.origin, this.router.url);
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(' ');
}));
}
/** /**
* Creates a researcher profile starting from an external source URI * Creates a researcher profile starting from an external source URI
* @param sourceUri URI of source item of researcher profile. * @param sourceUri URI of source item of researcher profile.
@@ -291,29 +189,6 @@ export class ResearcherProfileService {
return this.dataService.patch(researcherProfile, operations); return this.dataService.patch(researcherProfile, operations);
} }
/**
* 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()
);
}
} }

View File

@@ -15,6 +15,7 @@ import { By } from '@angular/platform-browser';
import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths'; import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
import { HandleService } from '../shared/handle.service';
describe('CurationFormComponent', () => { describe('CurationFormComponent', () => {
let comp: CurationFormComponent; let comp: CurationFormComponent;
@@ -23,6 +24,7 @@ describe('CurationFormComponent', () => {
let scriptDataService: ScriptDataService; let scriptDataService: ScriptDataService;
let processDataService: ProcessDataService; let processDataService: ProcessDataService;
let configurationDataService: ConfigurationDataService; let configurationDataService: ConfigurationDataService;
let handleService: HandleService;
let notificationsService; let notificationsService;
let router; let router;
@@ -51,6 +53,10 @@ describe('CurationFormComponent', () => {
})) }))
}); });
handleService = {
normalizeHandle: (a) => a
} as any;
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();
router = new RouterStub(); router = new RouterStub();
@@ -58,11 +64,12 @@ describe('CurationFormComponent', () => {
imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule], imports: [TranslateModule.forRoot(), FormsModule, ReactiveFormsModule],
declarations: [CurationFormComponent], declarations: [CurationFormComponent],
providers: [ providers: [
{provide: ScriptDataService, useValue: scriptDataService}, { provide: ScriptDataService, useValue: scriptDataService },
{provide: ProcessDataService, useValue: processDataService}, { provide: ProcessDataService, useValue: processDataService },
{provide: NotificationsService, useValue: notificationsService}, { provide: NotificationsService, useValue: notificationsService },
{provide: Router, useValue: router}, { provide: HandleService, useValue: handleService },
{provide: ConfigurationDataService, useValue: configurationDataService}, { provide: Router, useValue: router},
{ provide: ConfigurationDataService, useValue: configurationDataService },
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -143,4 +150,13 @@ describe('CurationFormComponent', () => {
{name: '-i', value: 'all'}, {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();
});
}); });

View File

@@ -5,7 +5,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { find, map } from 'rxjs/operators'; import { find, map } from 'rxjs/operators';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ProcessDataService } from '../core/data/processes/process-data.service'; 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 { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths'; 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'; export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
/** /**
* Component responsible for rendering the Curation Task form * Component responsible for rendering the Curation Task form
*/ */
@@ -39,6 +39,7 @@ export class CurationFormComponent implements OnInit {
private processDataService: ProcessDataService, private processDataService: ProcessDataService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private translateService: TranslateService, private translateService: TranslateService,
private handleService: HandleService,
private router: Router private router: Router
) { ) {
} }
@@ -76,13 +77,19 @@ export class CurationFormComponent implements OnInit {
const taskName = this.form.get('task').value; const taskName = this.form.get('task').value;
let handle; let handle;
if (this.hasHandleValue()) { 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 { } else {
handle = this.form.get('handle').value; handle = this.handleService.normalizeHandle(this.form.get('handle').value);
if (isEmpty(handle)) { if (isEmpty(handle)) {
handle = 'all'; handle = 'all';
} }
} }
this.scriptDataService.invoke('curate', [ this.scriptDataService.invoke('curate', [
{ name: '-t', value: taskName }, { name: '-t', value: taskName },
{ name: '-i', value: handle }, { name: '-i', value: handle },

View File

@@ -67,11 +67,11 @@
<a class="text-white" href="javascript:void(0);" <a class="text-white" href="javascript:void(0);"
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a> (click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
</li> </li>
<li> <li *ngIf="showPrivacyPolicy">
<a class="text-white" <a class="text-white"
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a> routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
</li> </li>
<li> <li *ngIf="showEndUserAgreement">
<a class="text-white" <a class="text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a> routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li> </li>

View File

@@ -1,6 +1,7 @@
import { Component, Optional } from '@angular/core'; import { Component, Optional } from '@angular/core';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { KlaroService } from '../shared/cookies/klaro.service'; import { KlaroService } from '../shared/cookies/klaro.service';
import { environment } from '../../environments/environment';
@Component({ @Component({
selector: 'ds-footer', selector: 'ds-footer',
@@ -14,6 +15,8 @@ export class FooterComponent {
* A boolean representing if to show or not the top footer container * A boolean representing if to show or not the top footer container
*/ */
showTopFooter = false; showTopFooter = false;
showPrivacyPolicy = environment.info.enablePrivacyStatement;
showEndUserAgreement = environment.info.enableEndUserAgreement;
constructor(@Optional() private cookies: KlaroService) { constructor(@Optional() private cookies: KlaroService) {
} }

View File

@@ -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"> <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-search-navbar></ds-search-navbar>
<ds-lang-switch></ds-lang-switch> <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> <ds-impersonate-navbar></ds-impersonate-navbar>
<div class="pl-2"> <div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()" <button class="navbar-toggler" type="button" (click)="toggleNavbar()"

View File

@@ -10,4 +10,4 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error> <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>

View File

@@ -1,6 +1,8 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ImportExternalPageComponent } from './import-external-page.component'; 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', () => { describe('ImportExternalPageComponent', () => {
let component: ImportExternalPageComponent; let component: ImportExternalPageComponent;
@@ -9,6 +11,9 @@ describe('ImportExternalPageComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ ImportExternalPageComponent ], declarations: [ ImportExternalPageComponent ],
providers:[
{ provide: ThemeService, useValue: getMockThemeService() },
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); .compileComponents();

View File

@@ -6,26 +6,10 @@ import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { FeedbackGuard } from '../core/feedback/feedback.guard'; import { FeedbackGuard } from '../core/feedback/feedback.guard';
import { environment } from '../../environments/environment';
@NgModule({ const imports = [
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' }
}
]),
RouterModule.forChild([ RouterModule.forChild([
{ {
path: FEEDBACK_PATH, path: FEEDBACK_PATH,
@@ -35,6 +19,34 @@ import { FeedbackGuard } from '../core/feedback/feedback.guard';
canActivate: [FeedbackGuard] 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
] ]
}) })
/** /**

View File

@@ -15,6 +15,7 @@ import { ItemPrivateComponent } from './item-private/item-private.component';
import { ItemPublicComponent } from './item-public/item-public.component'; import { ItemPublicComponent } from './item-public/item-public.component';
import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component';
import { ItemMetadataComponent } from './item-metadata/item-metadata.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 { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.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, ItemDeleteComponent,
ItemStatusComponent, ItemStatusComponent,
ItemMetadataComponent, ItemMetadataComponent,
ThemedItemMetadataComponent,
ItemRelationshipsComponent, ItemRelationshipsComponent,
ItemBitstreamsComponent, ItemBitstreamsComponent,
ItemVersionHistoryComponent, ItemVersionHistoryComponent,
@@ -84,7 +86,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
ObjectValuesPipe ObjectValuesPipe
], ],
exports: [ exports: [
ItemMetadataComponent EditInPlaceFieldComponent,
ThemedItemMetadataComponent,
] ]
}) })
export class EditItemPageModule { export class EditItemPageModule {

View File

@@ -44,7 +44,7 @@
class="alert alert-info w-100 d-inline-block mt-4" role="alert"> class="alert alert-info w-100 d-inline-block mt-4" role="alert">
{{'item.edit.bitstreams.empty' | translate}} {{'item.edit.bitstreams.empty' | translate}}
</div> </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="button-row bottom">
<div class="mt-4 float-right space-children-mr ml-gap"> <div class="mt-4 float-right space-children-mr ml-gap">

View File

@@ -50,3 +50,7 @@
cursor: grabbing; cursor: grabbing;
} }
} }
:host ::ng-deep .larger-tooltip .tooltip-inner {
max-width: 500px;
}

View File

@@ -29,5 +29,5 @@
</ng-container> </ng-container>
</div> </div>
</ng-container> </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> </ds-pagination>

View File

@@ -9,9 +9,10 @@
</div> </div>
<div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center"> <div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center">
<div class="w-100"> <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') }} {{ bitstream?.firstMetadataValue('dc.description') }}
</span> </div>
</div> </div>
</div> </div>
<div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center"> <div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center">

View File

@@ -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`);
}
}

View File

@@ -31,5 +31,5 @@
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div> <div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ds-loading *ngIf="loading$ | async"></ds-loading> <ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
</ng-container> </ng-container>

View File

@@ -32,7 +32,7 @@
></ds-edit-relationship-list> ></ds-edit-relationship-list>
</div> </div>
</ng-container> </ng-container>
<ds-loading *ngIf="!relationshipTypes"></ds-loading> <ds-themed-loading *ngIf="!relationshipTypes"></ds-themed-loading>
</ng-container> </ng-container>
<div class="button-row bottom"> <div class="button-row bottom">
<div class="float-right space-children-mr ml-gap"> <div class="float-right space-children-mr ml-gap">

View File

@@ -27,6 +27,8 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
import { RelationshipTypeService } from '../../../core/data/relationship-type.service'; import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock'; 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 comp: any;
let fixture: ComponentFixture<ItemRelationshipsComponent>; let fixture: ComponentFixture<ItemRelationshipsComponent>;
@@ -211,6 +213,7 @@ describe('ItemRelationshipsComponent', () => {
imports: [SharedModule, TranslateModule.forRoot()], imports: [SharedModule, TranslateModule.forRoot()],
declarations: [ItemRelationshipsComponent], declarations: [ItemRelationshipsComponent],
providers: [ providers: [
{ provide: ThemeService, useValue: getMockThemeService() },
{ provide: ItemDataService, useValue: itemService }, { provide: ItemDataService, useValue: itemService },
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: Router, useValue: router }, { provide: Router, useValue: router },

View File

@@ -30,7 +30,7 @@ export class CollectionsComponent implements OnInit {
label = 'item.page.collections'; label = 'item.page.collections';
separator = '<br/>'; @Input() separator = '<br/>';
/** /**
* Amount of mapped collections that should be fetched at once. * Amount of mapped collections that should be fetched at once.

View File

@@ -43,5 +43,5 @@
</div> </div>
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error> <ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
<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> </div>

View File

@@ -48,6 +48,7 @@ import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component
import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { OrcidPageComponent } from './orcid-page/orcid-page.component';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component'; 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 = [ const ENTRY_COMPONENTS = [
@@ -83,7 +84,8 @@ const DECLARATIONS = [
VersionPageComponent, VersionPageComponent,
OrcidPageComponent, OrcidPageComponent,
OrcidAuthComponent, OrcidAuthComponent,
OrcidSyncSettingsComponent OrcidSyncSettingsComponent,
OrcidQueueComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -1,9 +1,9 @@
<ng-container *ngVar="mediaList$ | async as mediaList"> <ng-container *ngVar="mediaList$ | async as mediaList">
<ds-loading <ds-themed-loading
*ngIf="isLoading" *ngIf="isLoading"
message="{{ 'loading.default' | translate }}" message="{{ 'loading.default' | translate }}"
[showMessage]="false" [showMessage]="false"
></ds-loading> ></ds-themed-loading>
<div class="media-viewer" *ngIf="!isLoading"> <div class="media-viewer" *ngIf="!isLoading">
<ng-container *ngIf="mediaList.length > 0"> <ng-container *ngIf="mediaList.length > 0">
<ng-container *ngIf="videoOptions"> <ng-container *ngIf="videoOptions">

View File

@@ -110,7 +110,7 @@ describe('MediaViewerComponent', () => {
}); });
it('should display a loading component', () => { 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(); expect(loading.nativeElement).toBeDefined();
}); });
}); });

View File

@@ -9,7 +9,7 @@ import { of } from 'rxjs';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
@@ -25,7 +25,7 @@ describe('OrcidAuthComponent test suite', () => {
let comp: OrcidAuthComponent; let comp: OrcidAuthComponent;
let fixture: ComponentFixture<OrcidAuthComponent>; let fixture: ComponentFixture<OrcidAuthComponent>;
let scheduler: TestScheduler; let scheduler: TestScheduler;
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>; let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
let nativeWindowRef; let nativeWindowRef;
let notificationsService; let notificationsService;
@@ -112,7 +112,7 @@ describe('OrcidAuthComponent test suite', () => {
}); });
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
researcherProfileService = jasmine.createSpyObj('researcherProfileService', { orcidAuthService = jasmine.createSpyObj('researcherProfileService', {
getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'), getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'),
getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'), getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'),
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'), getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'),
@@ -137,7 +137,7 @@ describe('OrcidAuthComponent test suite', () => {
providers: [ providers: [
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory },
{ provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: ResearcherProfileService, useValue: researcherProfileService } { provide: OrcidAuthService, useValue: orcidAuthService }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(OrcidAuthComponent, { }).overrideComponent(OrcidAuthComponent, {
@@ -149,17 +149,17 @@ describe('OrcidAuthComponent test suite', () => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
fixture = TestBed.createComponent(OrcidAuthComponent); fixture = TestBed.createComponent(OrcidAuthComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
researcherProfileService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes)); orcidAuthService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
})); }));
describe('when orcid profile is not linked', () => { describe('when orcid profile is not linked', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemUnlinkedToOrcid; comp.item = mockItemUnlinkedToOrcid;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
researcherProfileService.isLinkedToOrcid.and.returnValue(false); orcidAuthService.isLinkedToOrcid.and.returnValue(false);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
researcherProfileService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl')); orcidAuthService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl'));
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -183,7 +183,7 @@ describe('OrcidAuthComponent test suite', () => {
describe('when orcid profile is linked', () => { describe('when orcid profile is linked', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
})); }));
describe('', () => { describe('', () => {
@@ -191,16 +191,16 @@ describe('OrcidAuthComponent test suite', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
notificationsService = (comp as any).notificationsService; notificationsService = (comp as any).notificationsService;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
})); }));
describe('and unlink is successfully', () => { describe('and unlink is successfully', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile())); orcidAuthService.unlinkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile()));
spyOn(comp.unlink, 'emit'); spyOn(comp.unlink, 'emit');
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -217,7 +217,7 @@ describe('OrcidAuthComponent test suite', () => {
describe('and unlink is failed', () => { describe('and unlink is failed', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$()); orcidAuthService.unlinkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -234,10 +234,10 @@ describe('OrcidAuthComponent test suite', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -263,10 +263,10 @@ describe('OrcidAuthComponent test suite', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -294,10 +294,10 @@ describe('OrcidAuthComponent test suite', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false));
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -314,10 +314,10 @@ describe('OrcidAuthComponent test suite', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
comp.item = mockItemLinkedToOrcid; comp.item = mockItemLinkedToOrcid;
researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
researcherProfileService.isLinkedToOrcid.and.returnValue(true); orcidAuthService.isLinkedToOrcid.and.returnValue(true);
researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
fixture.detectChanges(); fixture.detectChanges();
})); }));

View File

@@ -3,14 +3,13 @@ import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, Simp
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
@Component({ @Component({
selector: 'ds-orcid-auth', selector: 'ds-orcid-auth',
@@ -65,7 +64,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
@Output() unlink: EventEmitter<void> = new EventEmitter<void>(); @Output() unlink: EventEmitter<void> = new EventEmitter<void>();
constructor( constructor(
private researcherProfileService: ResearcherProfileService, private orcidAuthService: OrcidAuthService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(NativeWindowService) private _window: NativeWindowRef,
@@ -73,7 +72,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
} }
ngOnInit() { ngOnInit() {
this.researcherProfileService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => { this.orcidAuthService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
this.orcidAuthorizationScopes.next(scopes); this.orcidAuthorizationScopes.next(scopes);
this.initOrcidAuthSettings(); this.initOrcidAuthSettings();
}); });
@@ -160,7 +159,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
* Link existing person profile with orcid * Link existing person profile with orcid
*/ */
linkOrcid(): void { linkOrcid(): void {
this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => { this.orcidAuthService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
this._window.nativeWindow.location.href = authorizeUrl; this._window.nativeWindow.location.href = authorizeUrl;
}); });
} }
@@ -170,7 +169,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
*/ */
unlinkOrcid(): void { unlinkOrcid(): void {
this.unlinkProcessing.next(true); this.unlinkProcessing.next(true);
this.researcherProfileService.unlinkOrcidByItem(this.item).pipe( this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((remoteData: RemoteData<ResearcherProfile>) => { ).subscribe((remoteData: RemoteData<ResearcherProfile>) => {
this.unlinkProcessing.next(false); this.unlinkProcessing.next(false);
@@ -193,19 +192,19 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
this.setMissingOrcidAuthorizations(); this.setMissingOrcidAuthorizations();
this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => { this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
this.onlyAdminCanDisconnectProfileFromOrcid$.next(result); this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
}); });
this.researcherProfileService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => { this.orcidAuthService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
this.ownerCanDisconnectProfileFromOrcid$.next(result); this.ownerCanDisconnectProfileFromOrcid$.next(result);
}); });
this.isOrcidLinked$.next(this.researcherProfileService.isLinkedToOrcid(this.item)); this.isOrcidLinked$.next(this.orcidAuthService.isLinkedToOrcid(this.item));
} }
private setMissingOrcidAuthorizations(): void { private setMissingOrcidAuthorizations(): void {
const profileScopes = this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item); const profileScopes = this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item);
const orcidScopes = this.orcidAuthorizationScopes.value; const orcidScopes = this.orcidAuthorizationScopes.value;
const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope)); const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope));
@@ -213,7 +212,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
} }
private setOrcidAuthorizationsFromItem(): void { private setOrcidAuthorizationsFromItem(): void {
this.profileAuthorizationScopes.next(this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item)); this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
} }
} }

View File

@@ -15,4 +15,5 @@
<ng-container *ngIf="!(processingConnection | async) && (item | async) && (connectionStatus | async)" > <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-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-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> </ng-container>

View File

@@ -11,7 +11,6 @@ import { getTestScheduler } from 'jasmine-marbles';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
import { OrcidPageComponent } from './orcid-page.component'; import { OrcidPageComponent } from './orcid-page.component';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
@@ -23,6 +22,7 @@ import { createPaginatedList } from '../../shared/testing/utils.test';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
describe('OrcidPageComponent test suite', () => { describe('OrcidPageComponent test suite', () => {
let comp: OrcidPageComponent; let comp: OrcidPageComponent;
@@ -32,7 +32,7 @@ describe('OrcidPageComponent test suite', () => {
let routeStub: jasmine.SpyObj<ActivatedRouteStub>; let routeStub: jasmine.SpyObj<ActivatedRouteStub>;
let routeData: any; let routeData: any;
let itemDataService: jasmine.SpyObj<ItemDataService>; let itemDataService: jasmine.SpyObj<ItemDataService>;
let researcherProfileService: jasmine.SpyObj<ResearcherProfileService>; let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
id: 'test-id', id: 'test-id',
@@ -88,7 +88,7 @@ describe('OrcidPageComponent test suite', () => {
routeStub = new ActivatedRouteStub({}, routeData); routeStub = new ActivatedRouteStub({}, routeData);
researcherProfileService = jasmine.createSpyObj('researcherProfileService', { orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'), isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'), linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
}); });
@@ -110,7 +110,7 @@ describe('OrcidPageComponent test suite', () => {
declarations: [OrcidPageComponent], declarations: [OrcidPageComponent],
providers: [ providers: [
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },
{ provide: ResearcherProfileService, useValue: researcherProfileService }, { provide: OrcidAuthService, useValue: orcidAuthService },
{ provide: AuthService, useValue: authService }, { provide: AuthService, useValue: authService },
{ provide: ItemDataService, useValue: itemDataService }, { provide: ItemDataService, useValue: itemDataService },
{ provide: PLATFORM_ID, useValue: 'browser' }, { provide: PLATFORM_ID, useValue: 'browser' },
@@ -146,7 +146,7 @@ describe('OrcidPageComponent test suite', () => {
it('should call isLinkedToOrcid', () => { it('should call isLinkedToOrcid', () => {
comp.isLinkedToOrcid(); comp.isLinkedToOrcid();
expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value); expect(orcidAuthService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value);
}); });
it('should update item', fakeAsync(() => { it('should update item', fakeAsync(() => {
@@ -168,13 +168,13 @@ describe('OrcidPageComponent test suite', () => {
describe('and linking to orcid profile is successfully', () => { describe('and linking to orcid profile is successfully', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
researcherProfileService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); orcidAuthService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid)); itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
fixture.detectChanges(); fixture.detectChanges();
})); }));
it('should call linkOrcidByItem', () => { it('should call linkOrcidByItem', () => {
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code'); expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
expect(comp.updateItem).toHaveBeenCalled(); expect(comp.updateItem).toHaveBeenCalled();
}); });
@@ -193,13 +193,13 @@ describe('OrcidPageComponent test suite', () => {
describe('and linking to orcid profile is failed', () => { describe('and linking to orcid profile is failed', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
researcherProfileService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$()); orcidAuthService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid)); itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
fixture.detectChanges(); fixture.detectChanges();
})); }));
it('should call linkOrcidByItem', () => { it('should call linkOrcidByItem', () => {
expect(researcherProfileService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code'); expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
expect(comp.updateItem).not.toHaveBeenCalled(); expect(comp.updateItem).not.toHaveBeenCalled();
}); });

View File

@@ -1,10 +1,11 @@
import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { BehaviorSubject, combineLatest } from 'rxjs'; import { BehaviorSubject, combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
@@ -14,7 +15,6 @@ import { redirectOn4xx } from '../../core/shared/authorized.operators';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
import { isPlatformBrowser } from '@angular/common';
/** /**
* A component that represents the orcid settings page * A component that represents the orcid settings page
@@ -50,7 +50,7 @@ export class OrcidPageComponent implements OnInit {
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,
private authService: AuthService, private authService: AuthService,
private itemService: ItemDataService, private itemService: ItemDataService,
private researcherProfileService: ResearcherProfileService, private orcidAuthService: OrcidAuthService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router private router: Router
) { ) {
@@ -95,7 +95,7 @@ export class OrcidPageComponent implements OnInit {
* @returns the check result * @returns the check result
*/ */
isLinkedToOrcid(): boolean { isLinkedToOrcid(): boolean {
return this.researcherProfileService.isLinkedToOrcid(this.item.value); return this.orcidAuthService.isLinkedToOrcid(this.item.value);
} }
/** /**
@@ -109,6 +109,7 @@ export class OrcidPageComponent implements OnInit {
* Retrieve the updated profile item * Retrieve the updated profile item
*/ */
updateItem(): void { updateItem(): void {
this.clearRouteParams();
this.itemService.findById(this.itemId, false).pipe( this.itemService.findById(this.itemId, false).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((itemRD: RemoteData<Item>) => { ).subscribe((itemRD: RemoteData<Item>) => {
@@ -125,7 +126,7 @@ export class OrcidPageComponent implements OnInit {
* @param code The auth-code received from ORCID * @param code The auth-code received from ORCID
*/ */
private linkProfileToOrcid(person: Item, code: string) { private linkProfileToOrcid(person: Item, code: string) {
this.researcherProfileService.linkOrcidByItem(person, code).pipe( this.orcidAuthService.linkOrcidByItem(person, code).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((profileRD: RemoteData<ResearcherProfile>) => { ).subscribe((profileRD: RemoteData<ResearcherProfile>) => {
this.processingConnection.next(false); this.processingConnection.next(false);
@@ -135,11 +136,18 @@ export class OrcidPageComponent implements OnInit {
} else { } else {
this.item.next(person); this.item.next(person);
this.connectionStatus.next(false); this.connectionStatus.next(false);
this.clearRouteParams();
}
});
} }
/**
* Update route removing the code from query params
* @private
*/
private clearRouteParams(): void {
// update route removing the code from query params // update route removing the code from query params
const redirectUrl = this.router.url.split('?')[0]; const redirectUrl = this.router.url.split('?')[0];
this.router.navigate([redirectUrl]); this.router.navigate([redirectUrl]);
});
} }
} }

View File

@@ -0,0 +1,51 @@
<div>
<ds-loading *ngIf="(processing$ | async)"></ds-loading>
<div class="container">
<h2>{{ 'person.orcid.registry.queue' | translate }}</h2>
<ds-alert *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements == 0"
[type]="AlertTypeEnum.Info">
{{ 'person.page.orcid.sync-queue.empty-message' | translate}}
</ds-alert>
<ds-pagination *ngIf="!(processing$ | async) && (getList() | async)?.payload?.totalElements > 0"
[paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements"
[retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
<div class="table-responsive">
<table id="groups" class="table table-sm table-striped table-hover table-bordered">
<thead>
<tr class="text-center align-middle">
<th>{{'person.page.orcid.sync-queue.table.header.type' | translate}}</th>
<th>{{'person.page.orcid.sync-queue.table.header.description' | translate}}</th>
<th>{{'person.page.orcid.sync-queue.table.header.action' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let entry of (getList() | async)?.payload?.page" data-test="orcidQueueElementRow">
<td style="width: 15%" class="text-center align-middle">
<i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate"
class="fa-2x" aria-hidden="true"></i>
</td>
<td class="text-center align-middle">
{{ entry.description }}
</td>
<td style="width: 20%" class="text-center">
<div class="btn-group edit-field">
<button [ngbTooltip]="getOperationTooltip(entry) | translate" container="body"
class="btn btn-outline-primary my-1 col-md" (click)="send(entry)">
<i [ngClass]="getOperationClass(entry)"></i>
</button>
<button [ngbTooltip]="'person.page.orcid.sync-queue.discard' | translate" container="body"
class="btn btn-outline-danger my-1 col-md" (click)="discardEntry(entry)">
<i class="fas fa-unlink"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
</div>
</div>

View File

@@ -0,0 +1,151 @@
import { OrcidQueueComponent } from './orcid-queue.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
import { RouterTestingModule } from '@angular/router/testing';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { By } from '@angular/platform-browser';
import { Item } from '../../../core/shared/item.model';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
describe('OrcidQueueComponent test suite', () => {
let component: OrcidQueueComponent;
let fixture: ComponentFixture<OrcidQueueComponent>;
let debugElement: DebugElement;
let orcidQueueService: OrcidQueueService;
let orcidAuthService: jasmine.SpyObj<OrcidAuthService>;
const testProfileItemId = 'test-owner-id';
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
}]
}
});
function orcidQueueElement(id: number) {
return Object.assign(new OrcidQueue(), {
'id': id,
'profileItemId': testProfileItemId,
'entityId': `test-entity-${id}`,
'description': `test description ${id}`,
'recordType': 'Publication',
'operation': 'INSERT',
'type': 'orcidqueue',
});
}
const orcidQueueElements = [orcidQueueElement(1), orcidQueueElement(2)];
const orcidQueueServiceSpy = jasmine.createSpyObj('orcidQueueService', ['searchByProfileItemId', 'clearFindByProfileItemRequests']);
orcidQueueServiceSpy.searchByProfileItemId.and.returnValue(createSuccessfulRemoteDataObject$<PaginatedList<OrcidQueue>>(createPaginatedList<OrcidQueue>(orcidQueueElements)));
beforeEach(waitForAsync(() => {
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl')
});
void TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
}),
RouterTestingModule.withRoutes([])
],
declarations: [OrcidQueueComponent],
providers: [
{ provide: OrcidAuthService, useValue: orcidAuthService },
{ provide: OrcidQueueService, useValue: orcidQueueServiceSpy },
{ provide: OrcidHistoryDataService, useValue: {} },
{ provide: PaginationService, useValue: new PaginationServiceStub() },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
orcidQueueService = TestBed.inject(OrcidQueueService);
}));
beforeEach(() => {
fixture = TestBed.createComponent(OrcidQueueComponent);
component = fixture.componentInstance;
component.item = mockItemLinkedToOrcid;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show the ORCID queue elements', () => {
const table = debugElement.queryAll(By.css('[data-test="orcidQueueElementRow"]'));
expect(table.length).toBe(2);
});
});

View File

@@ -0,0 +1,302 @@
import { Component, Input, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { OrcidHistory } from '../../../core/orcid/model/orcid-history.model';
import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { AlertType } from '../../../shared/alert/aletr-type';
import { Item } from '../../../core/shared/item.model';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
@Component({
selector: 'ds-orcid-queue',
templateUrl: './orcid-queue.component.html',
styleUrls: ['./orcid-queue.component.scss']
})
export class OrcidQueueComponent implements OnInit, OnDestroy {
/**
* The item for which showing the orcid settings
*/
@Input() item: Item;
/**
* Pagination config used to display the list
*/
public paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'oqp',
pageSize: 5
});
/**
* A boolean representing if results are loading
*/
public processing$ = new BehaviorSubject<boolean>(false);
/**
* A list of orcid queue records
*/
private list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any);
/**
* The AlertType enumeration
* @type {AlertType}
*/
AlertTypeEnum = AlertType;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
constructor(private orcidAuthService: OrcidAuthService,
private orcidQueueService: OrcidQueueService,
protected translateService: TranslateService,
private paginationService: PaginationService,
private notificationsService: NotificationsService,
private orcidHistoryService: OrcidHistoryDataService,
) {
}
ngOnInit(): void {
this.updateList();
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
this.updateList();
}
}
/**
* Retrieve queue list
*/
updateList() {
this.subs.push(
this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
debounceTime(100),
distinctUntilChanged(),
tap(() => this.processing$.next(true)),
switchMap((config: PaginationComponentOptions) => this.orcidQueueService.searchByProfileItemId(this.item.id, config, false)),
getFirstCompletedRemoteData()
).subscribe((result: RemoteData<PaginatedList<OrcidQueue>>) => {
this.processing$.next(false);
this.list$.next(result);
this.orcidQueueService.clearFindByProfileItemRequests();
})
);
}
/**
* Return the list of orcid queue records
*/
getList(): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
return this.list$.asObservable();
}
/**
* Return the icon class for the queue object type
*
* @param orcidQueue The OrcidQueue object
*/
getIconClass(orcidQueue: OrcidQueue): string {
if (!orcidQueue.recordType) {
return 'fa fa-user';
}
switch (orcidQueue.recordType.toLowerCase()) {
case 'publication':
return 'fas fa-book';
case 'project':
return 'fas fa-wallet';
case 'education':
return 'fas fa-school';
case 'affiliation':
return 'fas fa-university';
case 'country':
return 'fas fa-globe-europe';
case 'external_ids':
case 'researcher_urls':
return 'fas fa-external-link-alt';
default:
return 'fa fa-user';
}
}
/**
* Return the icon tooltip message for the queue object type
*
* @param orcidQueue The OrcidQueue object
*/
getIconTooltip(orcidQueue: OrcidQueue): string {
if (!orcidQueue.recordType) {
return '';
}
return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.recordType.toLowerCase();
}
/**
* Return the icon tooltip message for the queue object operation
*
* @param orcidQueue The OrcidQueue object
*/
getOperationTooltip(orcidQueue: OrcidQueue): string {
if (!orcidQueue.operation) {
return '';
}
return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.operation.toLowerCase();
}
/**
* Return the icon class for the queue object operation
*
* @param orcidQueue The OrcidQueue object
*/
getOperationClass(orcidQueue: OrcidQueue): string {
if (!orcidQueue.operation) {
return '';
}
switch (orcidQueue.operation.toLowerCase()) {
case 'insert':
return 'fas fa-plus';
case 'update':
return 'fas fa-edit';
case 'delete':
return 'fas fa-trash-alt';
default:
return '';
}
}
/**
* Discard a queue entry from the synchronization
*
* @param orcidQueue The OrcidQueue object to discard
*/
discardEntry(orcidQueue: OrcidQueue) {
this.processing$.next(true);
this.subs.push(this.orcidQueueService.deleteById(orcidQueue.id).pipe(
getFirstCompletedRemoteData()
).subscribe((remoteData) => {
this.processing$.next(false);
if (remoteData.isSuccess) {
this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.discard.success'));
this.updateList();
} else {
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.discard.error'));
}
}));
}
/**
* Send a queue entry to orcid for the synchronization
*
* @param orcidQueue The OrcidQueue object to synchronize
*/
send(orcidQueue: OrcidQueue) {
this.processing$.next(true);
this.subs.push(this.orcidHistoryService.sendToORCID(orcidQueue).pipe(
getFirstCompletedRemoteData()
).subscribe((remoteData) => {
this.processing$.next(false);
if (remoteData.isSuccess) {
this.handleOrcidHistoryRecordCreation(remoteData.payload);
} else if (remoteData.statusCode === 422) {
this.handleValidationErrors(remoteData);
} else {
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'));
}
}));
}
/**
* Return the error message for Unauthorized response
* @private
*/
private getUnauthorizedErrorContent(): Observable<string> {
return this.orcidAuthService.getOrcidAuthorizeUrl(this.item).pipe(
switchMap((authorizeUrl) => this.translateService.get(
'person.page.orcid.sync-queue.send.unauthorized-error.content',
{ orcid: authorizeUrl }
))
);
}
/**
* Manage notification by response
* @private
*/
private handleOrcidHistoryRecordCreation(orcidHistory: OrcidHistory) {
switch (orcidHistory.status) {
case 200:
case 201:
case 204:
this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.send.success'));
this.updateList();
break;
case 400:
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.bad-request-error'), null, { timeOut: 0 });
break;
case 401:
combineLatest([
this.translateService.get('person.page.orcid.sync-queue.send.unauthorized-error.title'),
this.getUnauthorizedErrorContent()],
).subscribe(([title, content]) => {
this.notificationsService.error(title, content, { timeOut: 0 }, true);
});
break;
case 404:
this.notificationsService.warning(this.translateService.get('person.page.orcid.sync-queue.send.not-found-warning'));
break;
case 409:
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.conflict-error'), null, { timeOut: 0 });
break;
default:
this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'), null, { timeOut: 0 });
}
}
/**
* Manage validation errors
* @private
*/
private handleValidationErrors(remoteData: RemoteData<OrcidHistory>) {
const translations = [this.translateService.get('person.page.orcid.sync-queue.send.validation-error')];
const errorMessage = remoteData.errorMessage;
if (errorMessage && errorMessage.indexOf('Error codes:') > 0) {
errorMessage.substring(errorMessage.indexOf(':') + 1).trim().split(',')
.forEach((error) => translations.push(this.translateService.get('person.page.orcid.sync-queue.send.validation-error.' + error)));
}
combineLatest(translations).subscribe((messages) => {
const title = messages.shift();
const content = '<ul>' + messages.map((message) => `<li>${message}</li>`).join('') + '</ul>';
this.notificationsService.error(title, content, { timeOut: 0 }, true);
});
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.list$ = null;
this.subs.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -6,7 +6,7 @@
<span>({{(file?.sizeBytes) | dsFileSize }})</span> <span>({{(file?.sizeBytes) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span> <span *ngIf="!last" innerHTML="{{separator}}"></span>
</ds-file-download-link> </ds-file-download-link>
<ds-loading *ngIf="isLoading" message="{{'loading.default' | translate}}" [showMessage]="false"></ds-loading> <ds-themed-loading *ngIf="isLoading" message="{{'loading.default' | translate}}" [showMessage]="false"></ds-themed-loading>
<div *ngIf="!isLastPage" class="mt-1" id="view-more"> <div *ngIf="!isLastPage" class="mt-1" id="view-more">
<a class="bitstream-view-more btn btn-outline-secondary btn-sm" [routerLink]="[]" (click)="getNextPage()">{{'item.page.bitstreams.view-more' | translate}}</a> <a class="bitstream-view-more btn btn-outline-secondary btn-sm" [routerLink]="[]" (click)="getNextPage()">{{'item.page.bitstreams.view-more' | translate}}</a>
</div> </div>

View File

@@ -86,7 +86,7 @@ describe('FileSectionComponent', () => {
}); });
it('should display a loading component', () => { 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(); expect(loading.nativeElement).toBeDefined();
}); });
}); });

View File

@@ -9,5 +9,5 @@
</div> </div>
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error> <ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
<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> </div>

View File

@@ -98,7 +98,7 @@ describe('ItemPageComponent', () => {
}); });
it('should display a loading component', () => { 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(); expect(loading.nativeElement).toBeDefined();
}); });
}); });

View File

@@ -4,7 +4,7 @@
<ds-metadata-representation-loader *ngFor="let rep of representations" <ds-metadata-representation-loader *ngFor="let rep of representations"
[mdRepresentation]="rep"> [mdRepresentation]="rep">
</ds-metadata-representation-loader> </ds-metadata-representation-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-loading> <ds-themed-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-themed-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0"> <div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0">
<div *ngIf="(objects.length * incrementBy) < total" class="float-left"> <div *ngIf="(objects.length * incrementBy) < total" class="float-left">
<a [routerLink]="[]" (click)="increase()">{{'item.page.related-items.view-more' | <a [routerLink]="[]" (click)="increase()">{{'item.page.related-items.view-more' |

View File

@@ -4,7 +4,7 @@
<ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page" <ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page"
[object]="item" [viewMode]="viewMode"> [object]="item" [viewMode]="viewMode">
</ds-listable-object-component-loader> </ds-listable-object-component-loader>
<ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading> <ds-themed-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-themed-loading>
<div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0"> <div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0">
<div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more"> <div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more">
<a [routerLink]="[]" (click)="increase()">{{'item.page.related-items.view-more' | <a [routerLink]="[]" (click)="increase()">{{'item.page.related-items.view-more' |

View File

@@ -11,8 +11,7 @@ import { MenuID } from '../../shared/menu/menu-id.model';
* Represents an expandable section in the navbar * Represents an expandable section in the navbar
*/ */
@Component({ @Component({
/* eslint-disable @angular-eslint/component-selector */ selector: 'ds-expandable-navbar-section',
selector: 'li[ds-expandable-navbar-section]',
templateUrl: './expandable-navbar-section.component.html', templateUrl: './expandable-navbar-section.component.html',
styleUrls: ['./expandable-navbar-section.component.scss'], styleUrls: ['./expandable-navbar-section.component.scss'],
animations: [slide] animations: [slide]

View File

@@ -0,0 +1,29 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
import { MenuID } from '../../shared/menu/menu-id.model';
/**
* Themed wrapper for ExpandableNavbarSectionComponent
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-themed-expandable-navbar-section]',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
@rendersSectionForMenu(MenuID.PUBLIC, true)
export class ThemedExpandableNavbarSectionComponent extends ThemedComponent<ExpandableNavbarSectionComponent> {
protected getComponentName(): string {
return 'ExpandableNavbarSectionComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/navbar/expandable-navbar-section/expandable-navbar-section.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./expandable-navbar-section.component`);
}
}

View File

@@ -4,7 +4,7 @@
<div class="container"> <div class="container">
<div class="reset-padding-md w-100"> <div class="reset-padding-md w-100">
<div id="collapsingNav"> <div id="collapsingNav">
<ul class="navbar-nav mr-auto shadow-none"> <ul class="navbar-nav navbar-navigation mr-auto shadow-none">
<ng-container *ngFor="let section of (sections | async)"> <ng-container *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container> <ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container> </ng-container>

View File

@@ -20,6 +20,8 @@ import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator'; import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { ThemeService } from '../shared/theme-support/theme.service';
import { getMockThemeService } from '../shared/mocks/theme-service.mock';
let comp: NavbarComponent; let comp: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>; let fixture: ComponentFixture<NavbarComponent>;
@@ -91,6 +93,7 @@ describe('NavbarComponent', () => {
declarations: [NavbarComponent], declarations: [NavbarComponent],
providers: [ providers: [
Injector, Injector,
{ provide: ThemeService, useValue: getMockThemeService() },
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: ActivatedRoute, useValue: routeStub }, { provide: ActivatedRoute, useValue: routeStub },

View File

@@ -7,6 +7,7 @@ import { BrowseService } from '../core/browse/browse.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../shared/menu/menu-id.model'; import { MenuID } from '../shared/menu/menu-id.model';
import { ThemeService } from '../shared/theme-support/theme.service';
/** /**
* Component representing the public navbar * Component representing the public navbar
@@ -29,9 +30,10 @@ export class NavbarComponent extends MenuComponent {
public windowService: HostWindowService, public windowService: HostWindowService,
public browseService: BrowseService, public browseService: BrowseService,
public authorizationService: AuthorizationDataService, public authorizationService: AuthorizationDataService,
public route: ActivatedRoute public route: ActivatedRoute,
protected themeService: ThemeService
) { ) {
super(menuService, injector, authorizationService, route); super(menuService, injector, authorizationService, route, themeService);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@@ -7,6 +7,7 @@ import { CoreModule } from '../core/core.module';
import { NavbarEffects } from './navbar.effects'; import { NavbarEffects } from './navbar.effects';
import { NavbarSectionComponent } from './navbar-section/navbar-section.component'; import { NavbarSectionComponent } from './navbar-section/navbar-section.component';
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section/expandable-navbar-section.component'; import { ExpandableNavbarSectionComponent } from './expandable-navbar-section/expandable-navbar-section.component';
import { ThemedExpandableNavbarSectionComponent } from './expandable-navbar-section/themed-expandable-navbar-section.component';
import { NavbarComponent } from './navbar.component'; import { NavbarComponent } from './navbar.component';
import { MenuModule } from '../shared/menu/menu.module'; import { MenuModule } from '../shared/menu/menu.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
@@ -20,7 +21,7 @@ const effects = [
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
NavbarSectionComponent, NavbarSectionComponent,
ExpandableNavbarSectionComponent, ThemedExpandableNavbarSectionComponent,
]; ];
@NgModule({ @NgModule({
@@ -36,13 +37,14 @@ const ENTRY_COMPONENTS = [
NavbarComponent, NavbarComponent,
ThemedNavbarComponent, ThemedNavbarComponent,
NavbarSectionComponent, NavbarSectionComponent,
ExpandableNavbarSectionComponent ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent,
], ],
providers: [], providers: [],
exports: [ exports: [
ThemedNavbarComponent, ThemedNavbarComponent,
NavbarSectionComponent, NavbarSectionComponent,
ExpandableNavbarSectionComponent ThemedExpandableNavbarSectionComponent
] ]
}) })

View File

@@ -38,7 +38,7 @@
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()"> <button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()">
{{ 'process.detail.logs.button' | translate }} {{ 'process.detail.logs.button' | translate }}
</button> </button>
<ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading> <ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
<pre class="font-weight-bold text-secondary bg-light p-3" <pre class="font-weight-bold text-secondary bg-light p-3"
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre> *ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs) <p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { RegisterEmailComponent } from './register-email.component';
/**
* Themed wrapper for RegisterEmailComponent
*/
@Component({
selector: 'ds-themed-register-email',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
export class ThemedRegisterEmailComponent extends ThemedComponent<RegisterEmailComponent> {
protected getComponentName(): string {
return 'RegisterEmailComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/register-page/register-email/register-email.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import('./register-email.component');
}
}

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { RegisterEmailComponent } from './register-email/register-email.component'; import { ThemedRegisterEmailComponent } from './register-email/themed-register-email.component';
import { ItemPageResolver } from '../item-page/item-page.resolver'; import { ItemPageResolver } from '../item-page/item-page.resolver';
import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard';
import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component'; import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component';
@@ -11,7 +11,7 @@ import { RegistrationGuard } from './registration.guard';
RouterModule.forChild([ RouterModule.forChild([
{ {
path: '', path: '',
component: RegisterEmailComponent, component: ThemedRegisterEmailComponent,
data: {title: 'register-email.title'}, data: {title: 'register-email.title'},
}, },
{ {

View File

@@ -7,6 +7,7 @@ import { CreateProfileComponent } from './create-profile/create-profile.componen
import { RegisterEmailFormModule } from '../register-email-form/register-email-form.module'; import { RegisterEmailFormModule } from '../register-email-form/register-email-form.module';
import { ProfilePageModule } from '../profile-page/profile-page.module'; import { ProfilePageModule } from '../profile-page/profile-page.module';
import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component'; import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component';
import { ThemedRegisterEmailComponent } from './register-email/themed-register-email.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -18,6 +19,7 @@ import { ThemedCreateProfileComponent } from './create-profile/themed-create-pro
], ],
declarations: [ declarations: [
RegisterEmailComponent, RegisterEmailComponent,
ThemedRegisterEmailComponent,
CreateProfileComponent, CreateProfileComponent,
ThemedCreateProfileComponent ThemedCreateProfileComponent
], ],

View File

@@ -5,5 +5,5 @@
<ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="deny($event)"></ds-email-request-copy> <ds-email-request-copy [subject]="subject$ | async" [message]="message$ | async" (send)="deny($event)"></ds-email-request-copy>
</div> </div>
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading> <ds-themed-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-themed-loading>
</div> </div>

View File

@@ -26,5 +26,5 @@
</p> </p>
</div> </div>
</div> </div>
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading> <ds-themed-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-themed-loading>
</div> </div>

View File

@@ -13,5 +13,5 @@
</form> </form>
</ds-email-request-copy> </ds-email-request-copy>
</div> </div>
<ds-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-loading> <ds-themed-loading *ngIf="!itemRequestRD || itemRequestRD?.isLoading"></ds-themed-loading>
</div> </div>

View File

@@ -9,7 +9,7 @@
<ds-themed-breadcrumbs></ds-themed-breadcrumbs> <ds-themed-breadcrumbs></ds-themed-breadcrumbs>
<div class="container d-flex justify-content-center align-items-center h-100" *ngIf="shouldShowRouteLoader"> <div class="container d-flex justify-content-center align-items-center h-100" *ngIf="shouldShowRouteLoader">
<ds-loading [showMessage]="false"></ds-loading> <ds-themed-loading [showMessage]="false"></ds-themed-loading>
</div> </div>
<div [class.d-none]="shouldShowRouteLoader"> <div [class.d-none]="shouldShowRouteLoader">
<router-outlet></router-outlet> <router-outlet></router-outlet>
@@ -24,5 +24,5 @@
</ds-notifications-board> </ds-notifications-board>
<div class="ds-full-screen-loader" *ngIf="shouldShowFullscreenLoader"> <div class="ds-full-screen-loader" *ngIf="shouldShowFullscreenLoader">
<ds-loading [showMessage]="false"></ds-loading> <ds-themed-loading [showMessage]="false"></ds-themed-loading>
</div> </div>

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../theme-support/themed.component';
import { AuthNavMenuComponent } from './auth-nav-menu.component';
/**
* Themed wrapper for {@link AuthNavMenuComponent}
*/
@Component({
selector: 'ds-themed-auth-nav-menu',
styleUrls: [],
templateUrl: '../theme-support/themed.component.html',
})
export class ThemedAuthNavMenuComponent extends ThemedComponent<AuthNavMenuComponent> {
protected getComponentName(): string {
return 'AuthNavMenuComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/shared/auth-nav-menu/auth-nav-menu.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./auth-nav-menu.component`);
}
}

View File

@@ -1,4 +1,4 @@
<ds-loading *ngIf="(loading$ | async)"></ds-loading> <ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading>
<div *ngIf="!(loading$ | async)"> <div *ngIf="!(loading$ | async)">
<span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span> <span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span>
<a class="dropdown-item" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a> <a class="dropdown-item" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>

View File

@@ -14,7 +14,7 @@
(next)="goNext()"> (next)="goNext()">
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="!objects || objects?.isLoading" message="{{'loading.browse-by' | translate}}"></ds-loading> <ds-themed-loading *ngIf="!objects || objects?.isLoading" message="{{'loading.browse-by' | translate}}"></ds-themed-loading>
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error> <ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
<div *ngIf="!objects?.isLoading && objects?.payload?.page.length === 0"> <div *ngIf="!objects?.isLoading && objects?.payload?.page.length === 0">
<div *ngIf="shouldDisplayResetButton$ |async" class="d-inline-block mb-4 reset"> <div *ngIf="shouldDisplayResetButton$ |async" class="d-inline-block mb-4 reset">

View File

@@ -17,7 +17,6 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { PaginationServiceStub } from '../testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { import {
ListableObjectComponentLoaderComponent ListableObjectComponentLoaderComponent
} from '../object-collection/shared/listable-object/listable-object-component-loader.component'; } from '../object-collection/shared/listable-object/listable-object-component-loader.component';
@@ -37,7 +36,6 @@ import { HostWindowServiceStub } from '../testing/host-window-service.stub';
import { HostWindowService } from '../host-window.service'; import { HostWindowService } from '../host-window.service';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { routeServiceStub } from '../testing/route-service.stub'; import { routeServiceStub } from '../testing/route-service.stub';
import SpyObj = jasmine.SpyObj;
import { GroupDataService } from '../../core/eperson/group-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service';
import { createPaginatedList } from '../testing/utils.test'; import { createPaginatedList } from '../testing/utils.test';
import { LinkHeadService } from '../../core/services/link-head.service'; import { LinkHeadService } from '../../core/services/link-head.service';
@@ -45,6 +43,7 @@ import { ConfigurationDataService } from '../../core/data/configuration-data.ser
import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../testing/search-configuration-service.stub';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { getMockThemeService } from '../mocks/theme-service.mock';
@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom') @listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom')
@Component({ @Component({
@@ -107,13 +106,10 @@ describe('BrowseByComponent', () => {
}); });
const paginationService = new PaginationServiceStub(paginationConfig); const paginationService = new PaginationServiceStub(paginationConfig);
let themeService: SpyObj<ThemeService>; let themeService;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
themeService = jasmine.createSpyObj('themeService', { themeService = getMockThemeService('dspace');
getThemeName: 'dspace',
getThemeName$: observableOf('dspace'),
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -142,19 +138,16 @@ describe('BrowseByComponent', () => {
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByComponent); fixture = TestBed.createComponent(BrowseByComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.paginationConfig = paginationConfig; comp.paginationConfig = paginationConfig;
fixture.detectChanges(); fixture.detectChanges();
}); }));
it('should display a loading message when objects is empty', () => { it('should display a loading message when objects is empty', () => {
(comp as any).objects = undefined; (comp as any).objects = undefined;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-loading'))).toBeDefined(); expect(fixture.debugElement.query(By.css('ds-themed-loading'))).toBeDefined();
}); });
it('should display results when objects is not empty', () => { it('should display results when objects is not empty', () => {

View File

@@ -35,8 +35,8 @@
</button> </button>
</ng-container> </ng-container>
<button class="dropdown-item disabled" *ngIf="(isLoading | async)"> <button class="dropdown-item disabled" *ngIf="(isLoading | async)">
<ds-loading message="{{'loading.default' | translate}}"> <ds-themed-loading message="{{'loading.default' | translate}}">
</ds-loading> </ds-themed-loading>
</button> </button>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More